From 43a8827c6ac1dca754bd1c35ea06619f0c42cd1b Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Fri, 25 Oct 2024 17:30:54 +0000 Subject: [PATCH 01/37] Change files --- ...-588b7294-cdfe-4c6c-8fea-6019b28165aa.json | 8 + .../src/CallClientState.ts | 78 +++-- .../src/CallContext.ts | 122 +++++++- .../src/CallFeatureStreamUtils.ts | 291 ++++++++++++++++++ .../src/CallSubscriber.ts | 1 + .../calling-stateful-client/src/Converter.ts | 26 +- .../src/InternalCallContext.ts | 75 +++++ .../src/StatefulCallClient.ts | 66 +++- .../src/StreamUtils.test.ts | 2 +- .../src/StreamUtils.ts | 28 +- .../src/TogetherModeSubscriber.ts | 102 +++++- .../src/TogetherModeVideoStreamSubscriber.ts | 62 ++++ .../src/index-public.ts | 8 +- .../review/beta/communication-react.api.md | 52 +++- .../CallComposite/MockCallAdapter.ts | 15 +- .../adapter/TestUtils.ts | 2 +- 16 files changed, 856 insertions(+), 82 deletions(-) create mode 100644 change-beta/@azure-communication-react-588b7294-cdfe-4c6c-8fea-6019b28165aa.json create mode 100644 packages/calling-stateful-client/src/CallFeatureStreamUtils.ts create mode 100644 packages/calling-stateful-client/src/TogetherModeVideoStreamSubscriber.ts diff --git a/change-beta/@azure-communication-react-588b7294-cdfe-4c6c-8fea-6019b28165aa.json b/change-beta/@azure-communication-react-588b7294-cdfe-4c6c-8fea-6019b28165aa.json new file mode 100644 index 00000000000..e3c34f2f586 --- /dev/null +++ b/change-beta/@azure-communication-react-588b7294-cdfe-4c6c-8fea-6019b28165aa.json @@ -0,0 +1,8 @@ +{ + "type": "prerelease", + "area": "feature", + "workstream": "togetherMode", + "comment": "Stream utils implementation for call feature streams. This also included the client state for together mode feature", + "packageName": "@azure/communication-react", + "dependentChangeType": "patch" +} diff --git a/packages/calling-stateful-client/src/CallClientState.ts b/packages/calling-stateful-client/src/CallClientState.ts index ff602e96432..344e1ce16de 100644 --- a/packages/calling-stateful-client/src/CallClientState.ts +++ b/packages/calling-stateful-client/src/CallClientState.ts @@ -272,45 +272,64 @@ export interface RaiseHandCallFeatureState { /* @conditional-compile-remove(together-mode) */ /** - * State only version of {@link @azure/communication-calling#TogetherModeCallFeature}. {@link StatefulCallClient} will - * automatically listen for raised hands on the call and update the state exposed by {@link StatefulCallClient} accordingly. - * @alpha + * @beta */ -export interface TogetherModeCallFeatureState { - /** - * Proxy of {@link @azure/communication-calling#TogetherModeCallFeature.togetherModeStream}. - */ - stream: TogetherModeStreamState[]; +export type CallFeatureStreamName = 'togetherMode'; + +/* @conditional-compile-remove(together-mode) */ +/** + * @beta + */ +export interface CallFeatureStreamState extends RemoteVideoStreamState { + feature?: CallFeatureStreamName; } /* @conditional-compile-remove(together-mode) */ /** - * State only version of {@link @azure/communication-calling#TogetherModeVideoStream}. - * @alpha + * State only version of {@link @azure/communication-calling#TogetherModeSeatingMap}. + * @beta + * + * Represents the seating position of a participant in Together Mode. */ -export interface TogetherModeStreamState { - /** - * Proxy of {@link @azure/communication-calling#TogetherModeVideoStream.id}. - */ - id: number; - /** - * Proxy of {@link @azure/communication-calling#TogetherModeVideoStream.mediaStreamType}. - */ - mediaStreamType: MediaStreamType; - /** - * Proxy of {@link @azure/communication-calling#TogetherModeVideoStream.isReceiving}. - * @public - */ - isReceiving: boolean; +export interface TogetherModeSeatingPositionState { + // The participant id of the participant in the seating position. + participantId: string; + // The top left offset from the top of the together mode view. + top: number; + // The left offset position from the left of the together mode view. + left: number; + // The width of the seating area + width: number; + // The height of the seating area. + height: number; +} + +/* @conditional-compile-remove(together-mode) */ +/** + * Interface representing the streams in Together Mode. + * + * @beta + */ +export interface TogetherModeStreamsState { + mainVideoStream?: CallFeatureStreamState; +} + +/* @conditional-compile-remove(together-mode) */ +/** + * State only version of {@link @azure/communication-calling#TogetherModeCallFeature}. {@link StatefulCallClient} will + * automatically listen for raised hands on the call and update the state exposed by {@link StatefulCallClient} accordingly. + * @beta + */ +export interface TogetherModeCallFeatureState { + isActive: boolean; /** - * {@link VideoStreamRendererView} that is managed by createView/disposeView in {@link StatefulCallClient} - * API. This can be undefined if the stream has not yet been rendered and defined after createView creates the view. + * Proxy of {@link @azure/communication-calling#TogetherModeCallFeature.togetherModeStream}. */ - view?: VideoStreamRendererViewState; + streams: TogetherModeStreamsState; /** - * Proxy of {@link @azure/communication-calling#RemoteVideoStream.size}. + * Proxy of {@link @azure/communication-calling#TogetherModeCallFeature.TogetherModeSeatingMap}. */ - streamSize?: { width: number; height: number }; + seatingPositions: TogetherModeSeatingPositionState[]; } /** @@ -621,6 +640,7 @@ export interface CallState { /* @conditional-compile-remove(together-mode) */ /** * Proxy of {@link @azure/communication-calling#TogetherModeCallFeature}. + * @beta */ togetherMode: TogetherModeCallFeatureState; /** diff --git a/packages/calling-stateful-client/src/CallContext.ts b/packages/calling-stateful-client/src/CallContext.ts index 0f29af497e5..bd8b8a12114 100644 --- a/packages/calling-stateful-client/src/CallContext.ts +++ b/packages/calling-stateful-client/src/CallContext.ts @@ -23,7 +23,7 @@ import { TeamsCaptionsInfo } from '@azure/communication-calling'; import { CaptionsKind, CaptionsInfo as AcsCaptionsInfo } from '@azure/communication-calling'; import { EnvironmentInfo } from '@azure/communication-calling'; /* @conditional-compile-remove(together-mode) */ -import { TogetherModeVideoStream } from '@azure/communication-calling'; +import { TogetherModeVideoStream, TogetherModeSeatingMap } from '@azure/communication-calling'; import { AzureLogger, createClientLogger, getLogLevel } from '@azure/logger'; import { EventEmitter } from 'events'; import { enableMapSet, enablePatches, Patch, produce } from 'immer'; @@ -65,6 +65,8 @@ import { SpotlightedParticipant } from '@azure/communication-calling'; import { LocalRecordingInfo } from '@azure/communication-calling'; /* @conditional-compile-remove(local-recording-notification) */ import { RecordingInfo } from '@azure/communication-calling'; +/* @conditional-compile-remove(together-mode) */ +import { CallFeatureStreamState, TogetherModeSeatingPositionState } from './CallClientState'; enableMapSet(); // Needed to generate state diff for verbose logging. @@ -455,11 +457,78 @@ export class CallContext { } /* @conditional-compile-remove(together-mode) */ - public setTogetherModeVideoStream(callId: string, addedStream: TogetherModeVideoStream[]): void { + public setTogetherModeVideoStreams( + callId: string, + addedStreams: CallFeatureStreamState[], + removedStreams: CallFeatureStreamState[] + ): void { + this.modifyState((draft: CallClientState) => { + const call = draft.calls[this._callIdHistory.latestCallId(callId)]; + if (call) { + for (const stream of removedStreams) { + if (stream.mediaStreamType === 'Video') { + call.togetherMode.streams.mainVideoStream = undefined; + call.togetherMode.isActive = false; + call.togetherMode.seatingPositions = []; + } + } + + for (const newStream of addedStreams) { + // This should only be called by the subscriber and some properties are add by other components so if the + // stream already exists, only update the values that subscriber knows about. + const mainVideoStream = call.togetherMode.streams.mainVideoStream; + if (mainVideoStream && mainVideoStream.id === newStream.id) { + mainVideoStream.mediaStreamType = newStream.mediaStreamType; + mainVideoStream.isAvailable = newStream.isAvailable; + mainVideoStream.isReceiving = newStream.isReceiving; + } else { + call.togetherMode.streams.mainVideoStream = newStream; + } + call.togetherMode.isActive = true; + } + } + }); + } + + /* @conditional-compile-remove(together-mode) */ + public setTogetherModeVideoStreamIsAvailable(callId: string, streamId: number, isAvailable: boolean): void { + this.modifyState((draft: CallClientState) => { + const call = draft.calls[this._callIdHistory.latestCallId(callId)]; + if (call) { + const stream = call.togetherMode.streams.mainVideoStream; + if (stream && stream?.id === streamId) { + stream.isReceiving = isAvailable; + } + } + }); + } + + /* @conditional-compile-remove(together-mode) */ + public setTogetherModeVideoStreamIsReceiving(callId: string, streamId: number, isReceiving: boolean): void { + this.modifyState((draft: CallClientState) => { + const call = draft.calls[this._callIdHistory.latestCallId(callId)]; + if (call) { + const stream = call.togetherMode.streams.mainVideoStream; + if (stream && stream?.id === streamId) { + stream.isReceiving = isReceiving; + } + } + }); + } + + /* @conditional-compile-remove(together-mode) */ + public setTogetherModeVideoStreamSize( + callId: string, + streamId: number, + size: { width: number; height: number } + ): void { this.modifyState((draft: CallClientState) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { - call.togetherMode = { stream: addedStream }; + const stream = call.togetherMode.streams.mainVideoStream; + if (stream && stream?.id === streamId) { + stream.streamSize = size; + } } }); } @@ -470,15 +539,37 @@ export class CallContext { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { for (const stream of removedStream) { - if (stream.mediaStreamType in call.togetherMode.stream) { - // Temporary lint fix: Remove the stream from the list - call.togetherMode.stream = []; + if (stream.mediaStreamType === 'Video') { + call.togetherMode.streams.mainVideoStream = undefined; + call.togetherMode.isActive = false; } } } }); } + /* @conditional-compile-remove(together-mode) */ + public setTogetherModeSeatingCoordinates(callId: string, seatingMap: TogetherModeSeatingMap): void { + this.modifyState((draft: CallClientState) => { + const call = draft.calls[this._callIdHistory.latestCallId(callId)]; + if (call) { + const seatingPositions: TogetherModeSeatingPositionState[] = []; + for (const [key, value] of seatingMap.entries()) { + const participantPosition: TogetherModeSeatingPositionState = { + participantId: key, + top: value.top, + left: value.left, + width: value.width, + height: value.height + }; + + seatingPositions.push(participantPosition); + } + call.togetherMode.seatingPositions = seatingPositions; + } + }); + } + public setCallRaisedHands(callId: string, raisedHands: RaisedHand[]): void { this.modifyState((draft: CallClientState) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; @@ -718,6 +809,25 @@ export class CallContext { }); } + /* @conditional-compile-remove(together-mode) */ + public setTogetherModeVideoStreamRendererView( + callId: string, + togetherModeStreamType: string, + view: VideoStreamRendererViewState | undefined + ): void { + this.modifyState((draft: CallClientState) => { + const call = draft.calls[this._callIdHistory.latestCallId(callId)]; + if (call) { + if (togetherModeStreamType === 'Video') { + const togetherModeStream = call.togetherMode.streams.mainVideoStream; + if (togetherModeStream) { + togetherModeStream.view = view; + } + } + } + }); + } + public setParticipantState(callId: string, participantKey: string, state: RemoteParticipantStatus): void { this.modifyState((draft: CallClientState) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; diff --git a/packages/calling-stateful-client/src/CallFeatureStreamUtils.ts b/packages/calling-stateful-client/src/CallFeatureStreamUtils.ts new file mode 100644 index 00000000000..12bd19efba3 --- /dev/null +++ b/packages/calling-stateful-client/src/CallFeatureStreamUtils.ts @@ -0,0 +1,291 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +/* @conditional-compile-remove(together-mode) */ +import { CreateViewOptions, VideoStreamRenderer } from '@azure/communication-calling'; +/* @conditional-compile-remove(together-mode) */ +import { CallContext } from './CallContext'; +/* @conditional-compile-remove(together-mode) */ +import { CallFeatureStreamState, CreateViewResult } from './index-public'; +/* @conditional-compile-remove(together-mode) */ +import { InternalCallContext } from './InternalCallContext'; +/* @conditional-compile-remove(together-mode) */ +import { _logStreamEvent } from './StreamUtilsLogging'; +/* @conditional-compile-remove(together-mode) */ +import { EventNames } from './Logger'; +/* @conditional-compile-remove(together-mode) */ +import { convertFromSDKToDeclarativeVideoStreamRendererView } from './Converter'; + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + * + */ +export function createCallFeatureView( + context: CallContext, + internalContext: InternalCallContext, + callId: string | undefined, + stream: CallFeatureStreamState, + options?: CreateViewOptions +): Promise { + const streamType = stream.mediaStreamType; + + if (callId && isCallFeatureStream(stream)) { + return createCallFeatureViewVideo(context, internalContext, callId, stream, options); + } else { + _logStreamEvent(EventNames.CREATE_STREAM_INVALID_PARAMS, { streamType }); + return Promise.resolve(undefined); + } +} + +/* @conditional-compile-remove(together-mode) */ +// This function is used to create a view for a stream that is part of a call feature. +async function createCallFeatureViewVideo( + context: CallContext, + internalContext: InternalCallContext, + callId: string, + stream: CallFeatureStreamState, + options?: CreateViewOptions +): Promise { + const streamEventType = 'createViewCallFeature'; + + const streamType = stream?.mediaStreamType; + const callFeatureStreamId = stream && stream.id; + const streamLogInfo = { + callId, + undefined, + streamId: callFeatureStreamId, + streamType, + streamEventType + }; + + // make different logging announcement based on whether or not we are starting a local or remote + _logStreamEvent(EventNames.CREATING_VIEW, streamLogInfo); + + const featureName = getStreamFeatureName(stream); + // if we have a participant Id and a stream get the remote info, else get the local render info from state. + const renderInfo = internalContext.getCallFeatureRenderInfo(callId, featureName, stream.mediaStreamType); + if (!renderInfo) { + _logStreamEvent(EventNames.STREAM_NOT_FOUND, streamLogInfo); + return; + } + if (renderInfo.status === 'Rendered') { + _logStreamEvent(EventNames.STREAM_ALREADY_RENDERED, streamLogInfo); + return; + } + if (renderInfo.status === 'Rendering') { + // Do not log to console here as this is a very common situation due to UI rerenders while + // the video rendering is in progress. + _logStreamEvent(EventNames.STREAM_RENDERING, streamLogInfo); + return; + } + + // "Stopping" only happens if the stream was in "rendering" but `disposeView` was called. + // Now that `createView` has been re-called, we can flip the state back to "rendering". + if (renderInfo.status === 'Stopping') { + _logStreamEvent(EventNames.STREAM_STOPPING, streamLogInfo); + internalContext.setCallFeatureRenderInfo( + callId, + featureName, + stream.mediaStreamType, + renderInfo.stream, + 'Rendering', + renderInfo.renderer + ); + return; + } + + const renderer = new VideoStreamRenderer(renderInfo.stream); + internalContext.setCallFeatureRenderInfo( + callId, + featureName, + stream.mediaStreamType, + renderInfo.stream, + 'Rendering', + undefined + ); + + let view; + try { + view = await renderer.createView(options); + } catch (e) { + _logStreamEvent(EventNames.CREATE_STREAM_FAIL, streamLogInfo, e); + internalContext.setCallFeatureRenderInfo( + callId, + featureName, + stream.mediaStreamType, + renderInfo.stream, + 'NotRendered', + undefined + ); + throw e; + } + + // Since render could take some time, we need to check if the stream is still valid and if we received a signal to + // stop rendering. + const refreshedRenderInfo = internalContext.getCallFeatureRenderInfo(callId, featureName, stream.mediaStreamType); + + if (!refreshedRenderInfo) { + // RenderInfo was removed. This should not happen unless stream was removed from the call so dispose the renderer + // and clean up the state. + _logStreamEvent(EventNames.RENDER_INFO_NOT_FOUND, streamLogInfo); + renderer.dispose(); + context.setTogetherModeVideoStreamRendererView(callId, stream.mediaStreamType, undefined); + return; + } + + if (refreshedRenderInfo.status === 'Stopping') { + // Stop render was called on this stream after we had started rendering. We will dispose this view and do not + // put the view into the state. + _logStreamEvent(EventNames.CREATED_STREAM_STOPPING, streamLogInfo); + renderer.dispose(); + internalContext.setCallFeatureRenderInfo( + callId, + featureName, + stream.mediaStreamType, + refreshedRenderInfo.stream, + 'NotRendered', + undefined + ); + context.setTogetherModeVideoStreamRendererView(callId, stream.mediaStreamType, undefined); + return; + } + + // Else the stream still exists and status is not telling us to stop rendering. Complete the render process by + // updating the state. + internalContext.setCallFeatureRenderInfo( + callId, + featureName, + stream.mediaStreamType, + refreshedRenderInfo.stream, + 'Rendered', + renderer + ); + context.setTogetherModeVideoStreamRendererView( + callId, + stream.mediaStreamType, + convertFromSDKToDeclarativeVideoStreamRendererView(view) + ); + _logStreamEvent(EventNames.VIEW_RENDER_SUCCEED, streamLogInfo); + + return { + renderer, + view + }; +} + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +export function disposeCallFeatureView( + context: CallContext, + internalContext: InternalCallContext, + callId: string | undefined, + stream: CallFeatureStreamState +): void { + const streamType = stream.mediaStreamType; + if (callId && isCallFeatureStream(stream)) { + return disposeCallFeatureViewVideo(context, internalContext, callId, stream); + } else { + _logStreamEvent(EventNames.DISPOSE_STREAM_INVALID_PARAMS, { streamType }); + return; + } +} + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +function disposeCallFeatureViewVideo( + context: CallContext, + internalContext: InternalCallContext, + callId: string, + stream: CallFeatureStreamState +): void { + const streamEventType = 'disposeViewCallFeature'; + + const streamType = stream.mediaStreamType; + const callFeatureStreamId = stream && stream.id; + + const streamLogInfo = { callId, undefined, streamId: callFeatureStreamId, streamType }; + + _logStreamEvent(EventNames.START_DISPOSE_STREAM, streamLogInfo); + + const featureName = getStreamFeatureName(stream); + + if (streamEventType === 'disposeViewCallFeature') { + context.setTogetherModeVideoStreamRendererView(callId, streamType, undefined); + } + + const renderInfo = internalContext.getCallFeatureRenderInfo(callId, featureName, stream.mediaStreamType); + if (!renderInfo) { + _logStreamEvent(EventNames.DISPOSE_INFO_NOT_FOUND, streamLogInfo); + return; + } + + // Nothing to dispose of or clean up -- we can safely exit early here. + if (renderInfo.status === 'NotRendered') { + _logStreamEvent(EventNames.STREAM_ALREADY_DISPOSED, streamLogInfo); + return; + } + + // Status is already marked as "stopping" so we can exit early here. This is because stopping only occurs + // when the stream is being created in createView but hasn't been completed being created yet. The createView + // method will see the "stopping" status and perform the cleanup + if (renderInfo.status === 'Stopping') { + _logStreamEvent(EventNames.STREAM_STOPPING, streamLogInfo); + return; + } + + // If the stream is in the middle of being rendered (i.e. has state "Rendering"), we need the status as + // "stopping" without performing any cleanup. This will tell the `createView` method that it should stop + // rendering and clean up the state once the view has finished being created. + if (renderInfo.status === 'Rendering') { + _logStreamEvent(EventNames.STREAM_STOPPING, streamLogInfo); + internalContext.setCallFeatureRenderInfo( + callId, + featureName, + streamType, + renderInfo.stream, + 'Stopping', + renderInfo.renderer + ); + return; + } + + if (renderInfo.renderer) { + _logStreamEvent(EventNames.DISPOSING_RENDERER, streamLogInfo); + renderInfo.renderer.dispose(); + // Else the state must be in the "Rendered" state, so we can dispose the renderer and clean up the state. + internalContext.setCallFeatureRenderInfo( + callId, + featureName, + streamType, + renderInfo.stream, + 'NotRendered', + undefined + ); + context.setTogetherModeVideoStreamRendererView(callId, streamType, undefined); + } else { + _logStreamEvent(EventNames.RENDERER_NOT_FOUND, streamLogInfo); + } +} + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +const getStreamFeatureName = (stream: CallFeatureStreamState): string => { + if (stream.feature) { + return stream.feature; + } + throw new Error('Feature name not found'); +}; + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +function isCallFeatureStream(stream: CallFeatureStreamState): boolean { + return 'feature' in stream || false; +} diff --git a/packages/calling-stateful-client/src/CallSubscriber.ts b/packages/calling-stateful-client/src/CallSubscriber.ts index c1d6ba7616b..983b439161e 100644 --- a/packages/calling-stateful-client/src/CallSubscriber.ts +++ b/packages/calling-stateful-client/src/CallSubscriber.ts @@ -127,6 +127,7 @@ export class CallSubscriber { this._togetherModeSubscriber = new TogetherModeSubscriber( this._callIdRef, this._context, + this._internalContext, this._call.feature(Features.TogetherMode) ); diff --git a/packages/calling-stateful-client/src/Converter.ts b/packages/calling-stateful-client/src/Converter.ts index d65140acd75..fa645853b8c 100644 --- a/packages/calling-stateful-client/src/Converter.ts +++ b/packages/calling-stateful-client/src/Converter.ts @@ -9,6 +9,9 @@ import { IncomingCall, IncomingCallCommon } from '@azure/communication-calling'; + +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeVideoStream as SdkTogetherModeVideoStream } from '@azure/communication-calling'; import { TeamsIncomingCall } from '@azure/communication-calling'; import { TeamsCaptionsInfo } from '@azure/communication-calling'; import { CaptionsInfo as AcsCaptionsInfo } from '@azure/communication-calling'; @@ -26,6 +29,8 @@ import { VideoStreamRendererViewState as DeclarativeVideoStreamRendererView, CallInfoState } from './CallClientState'; +/* @conditional-compile-remove(calling-beta-sdk) */ +import { CallFeatureStreamState as DeclarativeCallFeatureVideoStream, CallFeatureStreamName } from './CallClientState'; import { CaptionsInfo } from './CallClientState'; import { TeamsIncomingCallState as DeclarativeTeamsIncomingCall } from './CallClientState'; import { _isTeamsIncomingCall } from './TypeGuards'; @@ -73,6 +78,25 @@ export function convertSdkRemoteStreamToDeclarativeRemoteStream( }; } +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +export function convertSdkCallFeatureStreamToDeclarativeCallFeatureStream( + stream: SdkTogetherModeVideoStream, + featureName: CallFeatureStreamName +): DeclarativeCallFeatureVideoStream { + return { + feature: featureName, + id: stream.id, + mediaStreamType: stream.mediaStreamType, + isAvailable: stream.isAvailable, + isReceiving: stream.isReceiving, + view: undefined, + streamSize: stream.size + }; +} + /** * @private */ @@ -151,7 +175,7 @@ export function convertSdkCallToDeclarativeCall(call: CallCommon): CallState { pptLive: { isActive: false }, raiseHand: { raisedHands: [] }, /* @conditional-compile-remove(together-mode) */ - togetherMode: { stream: [] }, + togetherMode: { isActive: false, streams: {}, seatingPositions: [] }, localParticipantReaction: undefined, transcription: { isTranscriptionActive: false }, screenShareRemoteParticipant: undefined, diff --git a/packages/calling-stateful-client/src/InternalCallContext.ts b/packages/calling-stateful-client/src/InternalCallContext.ts index 500948dbf5b..a6e478c09cf 100644 --- a/packages/calling-stateful-client/src/InternalCallContext.ts +++ b/packages/calling-stateful-client/src/InternalCallContext.ts @@ -45,6 +45,12 @@ export type LocalRenderInfo = RenderInfo; */ export type RemoteRenderInfo = RenderInfo; +/* @conditional-compile-remove(together-mode) */ +/** + * Internally used to keep track of the status, renderer, and awaiting promise, associated with a CallFeatureVideoStream. + */ +export type CallFeatureRenderInfo = RenderInfo; + /** * Contains internal data used between different Declarative components to share data. */ @@ -55,6 +61,10 @@ export class InternalCallContext { // >. private _localRenderInfos = new Map>(); + /* @conditional-compile-remove(together-mode) */ + // >>. + private _callFeatureRenderInfos = new Map>>(); + // Used for keeping track of rendered LocalVideoStreams that are not part of a Call. private _unparentedRenderInfos = new Map(); private _callIdHistory = new CallIdHistory(); @@ -77,6 +87,13 @@ export class InternalCallContext { this._localRenderInfos.delete(oldCallId); this._localRenderInfos.set(newCallId, localRenderInfos); } + /* @conditional-compile-remove(together-mode) */ + const callFeatureRenderInfos = this._callFeatureRenderInfos.get(oldCallId); + /* @conditional-compile-remove(together-mode) */ + if (callFeatureRenderInfos) { + this._callFeatureRenderInfos.delete(oldCallId); + this._callFeatureRenderInfos.set(newCallId, callFeatureRenderInfos); + } } public getCallIds(): IterableIterator { @@ -221,5 +238,63 @@ export class InternalCallContext { public clearCallRelatedState(): void { this._remoteRenderInfos.clear(); this._localRenderInfos.clear(); + /* @conditional-compile-remove(together-mode) */ + this._callFeatureRenderInfos.clear(); + } + + /* @conditional-compile-remove(together-mode) */ + public getCallFeatureRenderInfosForCall( + callId: string + ): Map> | undefined { + return this._callFeatureRenderInfos.get(this._callIdHistory.latestCallId(callId)); + } + + /* @conditional-compile-remove(together-mode) */ + public getCallFeatureRenderInfo( + callId: string, + featureNameKey: string, + streamKey: MediaStreamType + ): CallFeatureRenderInfo | undefined { + const callFeatureRenderInfosForCall = this._callFeatureRenderInfos + .get(this._callIdHistory.latestCallId(callId)) + ?.get(featureNameKey) + ?.get(streamKey); + if (!callFeatureRenderInfosForCall) { + return undefined; + } + return callFeatureRenderInfosForCall; + } + + /* @conditional-compile-remove(together-mode) */ + public setCallFeatureRenderInfo( + callId: string, + featureNameKey: string, + streamKey: MediaStreamType, + stream: RemoteVideoStream, + status: RenderStatus, + renderer: VideoStreamRenderer | undefined + ): void { + let callRenderInfos = this._callFeatureRenderInfos.get(this._callIdHistory.latestCallId(callId)); + if (!callRenderInfos) { + callRenderInfos = new Map>(); + // If the callId is not found, create a new map for the callId. + this._callFeatureRenderInfos.set(this._callIdHistory.latestCallId(callId), callRenderInfos); + } + let featureRenderInfos = callRenderInfos.get(featureNameKey); + if (!featureRenderInfos) { + featureRenderInfos = new Map(); + callRenderInfos.set(featureNameKey, featureRenderInfos); + } + featureRenderInfos.set(streamKey, { stream, status, renderer }); + } + + /* @conditional-compile-remove(together-mode) */ + public deleteCallFeatureRenderInfo(callId: string, featureName: string, streamKey: MediaStreamType): void { + const callFeatureRenderInfoForCall = this._callFeatureRenderInfos.get(this._callIdHistory.latestCallId(callId)); + if (!callFeatureRenderInfoForCall || !callFeatureRenderInfoForCall.get(featureName)) { + return; + } + + callFeatureRenderInfoForCall.get(featureName)?.delete(streamKey); } } diff --git a/packages/calling-stateful-client/src/StatefulCallClient.ts b/packages/calling-stateful-client/src/StatefulCallClient.ts index a2f429e6a47..59201c69dcb 100644 --- a/packages/calling-stateful-client/src/StatefulCallClient.ts +++ b/packages/calling-stateful-client/src/StatefulCallClient.ts @@ -11,7 +11,7 @@ import { } from '@azure/communication-calling'; import { CallClientState, LocalVideoStreamState, RemoteVideoStreamState } from './CallClientState'; /* @conditional-compile-remove(together-mode) */ -import { TogetherModeStreamState } from './CallClientState'; +import { CallFeatureStreamState } from './CallClientState'; import { CallContext } from './CallContext'; import { callAgentDeclaratify, DeclarativeCallAgent } from './CallAgentDeclarative'; import { InternalCallContext } from './InternalCallContext'; @@ -26,6 +26,8 @@ import { callingStatefulLogger } from './Logger'; import { DeclarativeTeamsCallAgent, teamsCallAgentDeclaratify } from './TeamsCallAgentDeclarative'; import { MicrosoftTeamsUserIdentifier } from '@azure/communication-common'; import { videoStreamRendererViewDeclaratify } from './VideoStreamRendererViewDeclarative'; +/* @conditional-compile-remove(together-mode) */ +import { createCallFeatureView, disposeCallFeatureView } from './CallFeatureStreamUtils'; /** * Defines the methods that allow CallClient {@link @azure/communication-calling#CallClient} to be used statefully. @@ -115,10 +117,7 @@ export interface StatefulCallClient extends CallClient { createView( callId: string | undefined, participantId: CommunicationIdentifier | undefined, - stream: - | LocalVideoStreamState - | RemoteVideoStreamState - | /* @conditional-compile-remove(together-mode) */ TogetherModeStreamState, + stream: LocalVideoStreamState | RemoteVideoStreamState, options?: CreateViewOptions ): Promise; /** @@ -151,6 +150,44 @@ export interface StatefulCallClient extends CallClient { stream: LocalVideoStreamState | RemoteVideoStreamState ): void; + /* @conditional-compile-remove(together-mode) */ + /** + * Renders a {@link CallFeatureStreamState} + * {@link VideoStreamRendererViewState} under the relevant {@link CallFeatureStreamState} + * {@link @azure/communication-calling#VideoStreamRenderer.createView}. + * + * Scenario 1: Render CallFeatureStreamState + * - CallId is required and stream of type CallFeatureStreamState is required + * - Resulting {@link VideoStreamRendererViewState} is stored in the given callId and participantId in + * {@link CallClientState} + * + * @param callId - CallId for the given stream. Can be undefined if the stream is not part of any call. + * @param stream - The LocalVideoStreamState or RemoteVideoStreamState to start rendering. + * @param options - Options that are passed to the {@link @azure/communication-calling#VideoStreamRenderer}. + * @beta + */ + createCallFeatureView( + callId: string, + stream: CallFeatureStreamState, + options?: CreateViewOptions + ): Promise; + + /* @conditional-compile-remove(together-mode) */ + /** + * Stops rendering a {@link CallFeatureStreamState} and removes the + * {@link VideoStreamRendererView} from the relevant {@link CallFeatureStreamState} in {@link CallClientState} or + * {@link @azure/communication-calling#VideoStreamRenderer.dispose}. + * + * Its important to disposeView to clean up resources properly. + * + * Scenario 1: Dispose CallFeatureStreamState + * - CallId is required and stream of type CallFeatureStreamState is required + * + * @param callId - CallId for the given stream. Can be undefined if the stream is not part of any call. + * @param stream - The LocalVideoStreamState or RemoteVideoStreamState to dispose. + * @beta + */ + disposeCallFeatureView(callId: string, stream: CallFeatureStreamState): void; /** * The CallAgent is used to handle calls. * To create the CallAgent, pass a CommunicationTokenCredential object provided from SDK. @@ -399,6 +436,18 @@ export const createStatefulCallClientWithDeps = ( return result; } }); + /* @conditional-compile-remove(together-mode) */ + Object.defineProperty(callClient, 'createCallFeatureView', { + configurable: false, + value: async ( + callId: string | undefined, + stream: CallFeatureStreamState, + options?: CreateViewOptions + ): Promise => { + const result = await createCallFeatureView(context, internalContext, callId, stream, options); + return result; + } + }); Object.defineProperty(callClient, 'disposeView', { configurable: false, value: ( @@ -410,6 +459,13 @@ export const createStatefulCallClientWithDeps = ( disposeView(context, internalContext, callId, participantIdKind, stream); } }); + /* @conditional-compile-remove(together-mode) */ + Object.defineProperty(callClient, 'disposeCallFeatureView', { + configurable: false, + value: (callId: string | undefined, stream: CallFeatureStreamState): void => { + disposeCallFeatureView(context, internalContext, callId, stream); + } + }); const newStatefulCallClient = new Proxy( callClient, diff --git a/packages/calling-stateful-client/src/StreamUtils.test.ts b/packages/calling-stateful-client/src/StreamUtils.test.ts index 02dc616a08a..9840e19adb1 100644 --- a/packages/calling-stateful-client/src/StreamUtils.test.ts +++ b/packages/calling-stateful-client/src/StreamUtils.test.ts @@ -88,7 +88,7 @@ function createMockCall(mockCallId: string): CallState { localRecording: { isLocalRecordingActive: false }, raiseHand: { raisedHands: [] }, /* @conditional-compile-remove(together-mode) */ - togetherMode: { stream: [] }, + togetherMode: { isActive: false, streams: {}, seatingPositions: [] }, localParticipantReaction: undefined, transcription: { isTranscriptionActive: false }, screenShareRemoteParticipant: undefined, diff --git a/packages/calling-stateful-client/src/StreamUtils.ts b/packages/calling-stateful-client/src/StreamUtils.ts index 30d8339bc88..4ca1296e0b6 100644 --- a/packages/calling-stateful-client/src/StreamUtils.ts +++ b/packages/calling-stateful-client/src/StreamUtils.ts @@ -10,8 +10,6 @@ import { } from '@azure/communication-calling'; import { CommunicationIdentifierKind } from '@azure/communication-common'; import { LocalVideoStreamState, RemoteVideoStreamState } from './CallClientState'; -/* @conditional-compile-remove(together-mode) */ -import { TogetherModeStreamState } from './CallClientState'; import { CallContext } from './CallContext'; import { convertSdkLocalStreamToDeclarativeLocalStream, @@ -37,10 +35,7 @@ async function createViewVideo( context: CallContext, internalContext: InternalCallContext, callId: string, - stream?: - | RemoteVideoStreamState - | LocalVideoStreamState - | /* @conditional-compile-remove(together-mode) */ TogetherModeStreamState, + stream?: RemoteVideoStreamState | LocalVideoStreamState, participantId?: CommunicationIdentifierKind | string, options?: CreateViewOptions ): Promise { @@ -493,10 +488,7 @@ export function createView( internalContext: InternalCallContext, callId: string | undefined, participantId: CommunicationIdentifierKind | string | undefined, - stream: - | LocalVideoStreamState - | RemoteVideoStreamState - | /* @conditional-compile-remove(together-mode) */ TogetherModeStreamState, + stream: LocalVideoStreamState | RemoteVideoStreamState, options?: CreateViewOptions ): Promise { const streamType = stream.mediaStreamType; @@ -586,6 +578,22 @@ export function disposeAllViewsFromCall( } } } + /* @conditional-compile-remove(together-mode) */ + const callFeatureStreams = internalContext.getCallFeatureRenderInfosForCall(callId); + /* @conditional-compile-remove(together-mode) */ + if (callFeatureStreams) { + for (const [, featureStreams] of callFeatureStreams.entries()) { + for (const [, streamAndRenderer] of featureStreams.entries()) { + disposeView( + context, + internalContext, + callId, + undefined, + convertSdkRemoteStreamToDeclarativeRemoteStream(streamAndRenderer.stream as RemoteVideoStream) + ); + } + } + } } /** diff --git a/packages/calling-stateful-client/src/TogetherModeSubscriber.ts b/packages/calling-stateful-client/src/TogetherModeSubscriber.ts index 64f791becc0..ab9e956d130 100644 --- a/packages/calling-stateful-client/src/TogetherModeSubscriber.ts +++ b/packages/calling-stateful-client/src/TogetherModeSubscriber.ts @@ -2,15 +2,26 @@ // Licensed under the MIT License. /* @conditional-compile-remove(together-mode) */ -import { TogetherModeCallFeature, TogetherModeVideoStream } from '@azure/communication-calling'; +import { + RemoteVideoStream, + TogetherModeCallFeature, + TogetherModeSeatingMap, + TogetherModeVideoStream +} from '@azure/communication-calling'; /* @conditional-compile-remove(together-mode) */ import { CallContext } from './CallContext'; /* @conditional-compile-remove(together-mode) */ import { CallIdRef } from './CallIdRef'; -/** - * @private - */ - +/* @conditional-compile-remove(together-mode) */ +import { InternalCallContext } from './InternalCallContext'; +/* @conditional-compile-remove(together-mode) */ +import { disposeCallFeatureView } from './CallFeatureStreamUtils'; +/* @conditional-compile-remove(together-mode) */ +import { convertSdkCallFeatureStreamToDeclarativeCallFeatureStream } from './Converter'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeVideoStreamSubscriber } from './TogetherModeVideoStreamSubscriber'; +/* @conditional-compile-remove(together-mode) */ +import { CallFeatureStreamName } from './CallClientState'; /* @conditional-compile-remove(together-mode) */ /** * TogetherModeSubscriber is responsible for subscribing to together mode events and updating the call context accordingly. @@ -18,33 +29,98 @@ import { CallIdRef } from './CallIdRef'; export class TogetherModeSubscriber { private _callIdRef: CallIdRef; private _context: CallContext; + private _internalContext: InternalCallContext; private _togetherMode: TogetherModeCallFeature; + private _featureName: CallFeatureStreamName = 'togetherMode'; + private _togetherModeVideoStreamSubscribers: Map; - constructor(callIdRef: CallIdRef, context: CallContext, togetherMode: TogetherModeCallFeature) { + constructor( + callIdRef: CallIdRef, + context: CallContext, + internalContext: InternalCallContext, + togetherMode: TogetherModeCallFeature + ) { this._callIdRef = callIdRef; this._context = context; + this._internalContext = internalContext; this._togetherMode = togetherMode; - + this._togetherModeVideoStreamSubscribers = new Map(); this.subscribe(); } private subscribe = (): void => { this._togetherMode.on('togetherModeStreamsUpdated', this.onTogetherModeStreamUpdated); + this._togetherMode.on('togetherModeSceneUpdated', this.onSceneUpdated); + this._togetherMode.on('togetherModeSeatingUpdated', this.onSeatUpdated); }; public unsubscribe = (): void => { this._togetherMode.off('togetherModeStreamsUpdated', this.onTogetherModeStreamUpdated); + this._togetherMode.off('togetherModeSceneUpdated', this.onSceneUpdated); + this._togetherMode.off('togetherModeSeatingUpdated', this.onSeatUpdated); + }; + + private onSceneUpdated = (args: TogetherModeSeatingMap): void => { + this._context.setTogetherModeSeatingCoordinates(this._callIdRef.callId, args); + }; + + private onSeatUpdated = (args: TogetherModeSeatingMap): void => { + this._context.setTogetherModeSeatingCoordinates(this._callIdRef.callId, args); + }; + + private addRemoteVideoStreamSubscriber = (togetherModeVideoStream: TogetherModeVideoStream): void => { + this._togetherModeVideoStreamSubscribers.get(togetherModeVideoStream.id)?.unsubscribe(); + this._togetherModeVideoStreamSubscribers.set( + togetherModeVideoStream.id, + new TogetherModeVideoStreamSubscriber(this._callIdRef, togetherModeVideoStream, this._context) + ); + }; + + private updateTogetherModeStreams = ( + addedStreams: TogetherModeVideoStream[], + removedStreams: TogetherModeVideoStream[] + ): void => { + for (const stream of removedStreams) { + this._togetherModeVideoStreamSubscribers.get(stream.id)?.unsubscribe(); + disposeCallFeatureView( + this._context, + this._internalContext, + this._callIdRef.callId, + convertSdkCallFeatureStreamToDeclarativeCallFeatureStream(stream, this._featureName) + ); + this._internalContext.deleteCallFeatureRenderInfo( + this._callIdRef.callId, + this._featureName, + stream.mediaStreamType + ); + } + + for (const stream of addedStreams) { + this._internalContext.setCallFeatureRenderInfo( + this._callIdRef.callId, + this._featureName, + stream.mediaStreamType, + stream as RemoteVideoStream, + 'NotRendered', + undefined + ); + this.addRemoteVideoStreamSubscriber(stream); + } + this._context.setTogetherModeVideoStreams( + this._callIdRef.callId, + addedStreams.map((stream) => + convertSdkCallFeatureStreamToDeclarativeCallFeatureStream(stream, this._featureName) + ), + removedStreams.map((stream) => + convertSdkCallFeatureStreamToDeclarativeCallFeatureStream(stream, this._featureName) + ) + ); }; private onTogetherModeStreamUpdated = (args: { added: TogetherModeVideoStream[]; removed: TogetherModeVideoStream[]; }): void => { - if (args.added) { - this._context.setTogetherModeVideoStream(this._callIdRef.callId, args.added); - } - if (args.removed) { - this._context.removeTogetherModeVideoStream(this._callIdRef.callId, args.removed); - } + this.updateTogetherModeStreams(args.added, args.removed); }; } diff --git a/packages/calling-stateful-client/src/TogetherModeVideoStreamSubscriber.ts b/packages/calling-stateful-client/src/TogetherModeVideoStreamSubscriber.ts new file mode 100644 index 00000000000..5ce633273ce --- /dev/null +++ b/packages/calling-stateful-client/src/TogetherModeVideoStreamSubscriber.ts @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeVideoStream } from '@azure/communication-calling'; +/* @conditional-compile-remove(together-mode) */ +import { CallContext } from './CallContext'; +/* @conditional-compile-remove(together-mode) */ +import { CallIdRef } from './CallIdRef'; + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +export class TogetherModeVideoStreamSubscriber { + private _callIdRef: CallIdRef; + private _togetherModeStream: TogetherModeVideoStream; + private _context: CallContext; + + constructor(callIdRef: CallIdRef, stream: TogetherModeVideoStream, context: CallContext) { + this._callIdRef = callIdRef; + this._togetherModeStream = stream; + this._context = context; + this.subscribe(); + } + + private subscribe = (): void => { + this._togetherModeStream.on('isAvailableChanged', this.isAvailableChanged); + this._togetherModeStream.on('isReceivingChanged', this.isReceivingChanged); + this._togetherModeStream.on('sizeChanged', this.isSizeChanged); + }; + + public unsubscribe = (): void => { + this._togetherModeStream.off('isAvailableChanged', this.isAvailableChanged); + this._togetherModeStream.off('isReceivingChanged', this.isReceivingChanged); + this._togetherModeStream.off('sizeChanged', this.isSizeChanged); + }; + + private isAvailableChanged = (): void => { + this._context.setTogetherModeVideoStreamIsAvailable( + this._callIdRef.callId, + this._togetherModeStream.id, + this._togetherModeStream.isAvailable + ); + }; + + private isReceivingChanged = (): void => { + this._context.setTogetherModeVideoStreamIsReceiving( + this._callIdRef.callId, + this._togetherModeStream.id, + this._togetherModeStream.isReceiving + ); + }; + + private isSizeChanged = (): void => { + this._context.setTogetherModeVideoStreamSize( + this._callIdRef.callId, + this._togetherModeStream.id, + this._togetherModeStream.size + ); + }; +} diff --git a/packages/calling-stateful-client/src/index-public.ts b/packages/calling-stateful-client/src/index-public.ts index 72fc02446d6..2fef80e56af 100644 --- a/packages/calling-stateful-client/src/index-public.ts +++ b/packages/calling-stateful-client/src/index-public.ts @@ -34,7 +34,13 @@ export type { RaiseHandCallFeatureState as RaiseHandCallFeature } from './CallCl /* @conditional-compile-remove(together-mode) */ export type { TogetherModeCallFeatureState as TogetherModeCallFeature } from './CallClientState'; /* @conditional-compile-remove(together-mode) */ -export type { TogetherModeStreamState } from './CallClientState'; +export type { + CallFeatureStreamState, + TogetherModeSeatingPositionState, + CallFeatureStreamName, + TogetherModeStreamsState +} from './CallClientState'; + export type { RaisedHandState } from './CallClientState'; export type { DeclarativeCallAgent, IncomingCallManagement } from './CallAgentDeclarative'; export type { DeclarativeTeamsCallAgent } from './TeamsCallAgentDeclarative'; diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index cbbbed7f4de..987420b72d0 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -1036,6 +1036,15 @@ export type CallErrors = { // @public export type CallErrorTarget = 'Call.addParticipant' | 'Call.dispose' | 'Call.feature' | 'Call.hangUp' | 'Call.hold' | 'Call.mute' | 'Call.muteIncomingAudio' | 'Call.off' | 'Call.on' | 'Call.removeParticipant' | 'Call.resume' | 'Call.sendDtmf' | 'Call.startAudio' | 'Call.startScreenSharing' | 'Call.startVideo' | 'Call.stopScreenSharing' | 'Call.stopAudio' | 'Call.stopVideo' | 'Call.unmute' | 'Call.unmuteIncomingAudio' | 'CallAgent.dispose' | 'CallAgent.feature' | 'CallAgent.join' | 'CallAgent.off' | 'CallAgent.on' | 'CallAgent.startCall' | 'CallClient.createCallAgent' | 'CallClient.createTeamsCallAgent' | 'CallClient.feature' | 'CallClient.getDeviceManager' | 'CallClient.getEnvironmentInfo' | 'DeviceManager.askDevicePermission' | 'DeviceManager.getCameras' | 'DeviceManager.getMicrophones' | 'DeviceManager.getSpeakers' | 'DeviceManager.off' | 'DeviceManager.on' | 'DeviceManager.selectMicrophone' | 'DeviceManager.selectSpeaker' | 'IncomingCall.accept' | 'IncomingCall.reject' | 'TeamsCall.addParticipant' | 'VideoEffectsFeature.startEffects' | /* @conditional-compile-remove(calling-beta-sdk) */ 'CallAgent.handlePushNotification' | /* @conditional-compile-remove(calling-beta-sdk) */ 'Call.admit' | /* @conditional-compile-remove(calling-beta-sdk) */ 'Call.rejectParticipant' | /* @conditional-compile-remove(calling-beta-sdk) */ 'Call.admitAll' | 'Call.mutedByOthers' | 'Call.muteAllRemoteParticipants' | 'Call.setConstraints'; +// @beta (undocumented) +export type CallFeatureStreamName = 'togetherMode'; + +// @beta (undocumented) +export interface CallFeatureStreamState extends RemoteVideoStreamState { + // (undocumented) + feature?: CallFeatureStreamName; +} + // @public export type CallIdChangedListener = (event: { callId: string; @@ -1168,6 +1177,7 @@ export interface CallState { spotlight?: SpotlightCallFeatureState; startTime: Date; state: CallState_2; + // @beta togetherMode: TogetherModeCallFeature; totalParticipantCount?: number; transcription: TranscriptionCallFeature; @@ -4684,8 +4694,12 @@ export type StartTeamsCallIdentifier = MicrosoftTeamsUserIdentifier | PhoneNumbe // @public export interface StatefulCallClient extends CallClient { createCallAgent(...args: Parameters): Promise; + // @beta + createCallFeatureView(callId: string, stream: CallFeatureStreamState, options?: CreateViewOptions): Promise; createTeamsCallAgent(...args: Parameters): Promise; - createView(callId: string | undefined, participantId: CommunicationIdentifier | undefined, stream: LocalVideoStreamState | RemoteVideoStreamState | /* @conditional-compile-remove(together-mode) */ TogetherModeStreamState, options?: CreateViewOptions): Promise; + createView(callId: string | undefined, participantId: CommunicationIdentifier | undefined, stream: LocalVideoStreamState | RemoteVideoStreamState, options?: CreateViewOptions): Promise; + // @beta + disposeCallFeatureView(callId: string, stream: CallFeatureStreamState): void; disposeView(callId: string | undefined, participantId: CommunicationIdentifier | undefined, stream: LocalVideoStreamState | RemoteVideoStreamState): void; getState(): CallClientState; offStateChange(handler: (state: CallClientState) => void): void; @@ -4871,22 +4885,32 @@ export type TeamsOutboundCallAdapterArgs = TeamsCallAdapterArgsCommon & { // @public export const toFlatCommunicationIdentifier: (identifier: CommunicationIdentifier) => string; -// @alpha +// @beta export interface TogetherModeCallFeature { - stream: TogetherModeStreamState[]; + // (undocumented) + isActive: boolean; + seatingPositions: TogetherModeSeatingPositionState[]; + streams: TogetherModeStreamsState; } -// @alpha -export interface TogetherModeStreamState { - id: number; - // @public - isReceiving: boolean; - mediaStreamType: MediaStreamType; - streamSize?: { - width: number; - height: number; - }; - view?: VideoStreamRendererViewState; +// @beta +export interface TogetherModeSeatingPositionState { + // (undocumented) + height: number; + // (undocumented) + left: number; + // (undocumented) + participantId: string; + // (undocumented) + top: number; + // (undocumented) + width: number; +} + +// @beta +export interface TogetherModeStreamsState { + // (undocumented) + mainVideoStream?: CallFeatureStreamState; } // @public diff --git a/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts b/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts index 40c6dec15de..288c26e7e79 100644 --- a/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts @@ -110,9 +110,18 @@ export class _MockCallAdapter implements CallAdapter { createStreamView(): Promise { throw Error('createStreamView not implemented'); } + /* @conditional-compile-remove(together-mode) */ + createTogetherModeStreamViews(): Promise { + throw Error('createTogetherModeStreamViews not implemented'); + } + /* @conditional-compile-remove(together-mode) */ startTogetherMode(): Promise { throw Error('startTogetherMode not implemented'); } + /* @conditional-compile-remove(together-mode) */ + setTogetherModeSceneSize(width: number, height: number): void { + throw Error(`Setting Together Mode scene to width ${width} and height ${height} is not implemented`); + } disposeStreamView(): Promise { return Promise.resolve(); } @@ -125,6 +134,10 @@ export class _MockCallAdapter implements CallAdapter { disposeRemoteVideoStreamView(): Promise { return Promise.resolve(); } + /* @conditional-compile-remove(together-mode) */ + disposeTogetherModeStreamViews(): Promise { + return Promise.resolve(); + } // eslint-disable-next-line @typescript-eslint/no-unused-vars askDevicePermission(constrain: PermissionConstraints): Promise { throw Error('askDevicePermission not implemented'); @@ -257,7 +270,7 @@ const createDefaultCallAdapterState = (role?: ParticipantRole): CallAdapterState remoteParticipantsEnded: {}, raiseHand: { raisedHands: [] }, /* @conditional-compile-remove(together-mode) */ - togetherMode: { stream: [] }, + togetherMode: { isActive: false, streams: {}, seatingPositions: [] }, pptLive: { isActive: false }, localParticipantReaction: undefined, role, diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/TestUtils.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/TestUtils.ts index 5d9a8d99e59..79cb9a2c15b 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/TestUtils.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/TestUtils.ts @@ -240,7 +240,7 @@ function createMockCall(mockCallId: string): CallState { dominantSpeakers: undefined, raiseHand: { raisedHands: [] }, /* @conditional-compile-remove(together-mode) */ - togetherMode: { stream: [] }, + togetherMode: { isActive: false, streams: {}, seatingPositions: [] }, pptLive: { isActive: false }, localParticipantReaction: undefined, captionsFeature: { From 0280dd8dc4fc369cae193177e51fa3fbc4b9606f Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Fri, 25 Oct 2024 17:53:47 +0000 Subject: [PATCH 02/37] Included a common util method that set call features renderer info --- ...-fccab07a-9214-4608-9852-dde7be8f0087.json | 9 +++++ .../src/CallFeatureStreamUtils.ts | 39 +++++++++++++++---- 2 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 change-beta/@azure-communication-react-fccab07a-9214-4608-9852-dde7be8f0087.json diff --git a/change-beta/@azure-communication-react-fccab07a-9214-4608-9852-dde7be8f0087.json b/change-beta/@azure-communication-react-fccab07a-9214-4608-9852-dde7be8f0087.json new file mode 100644 index 00000000000..6a34cfa6ce4 --- /dev/null +++ b/change-beta/@azure-communication-react-fccab07a-9214-4608-9852-dde7be8f0087.json @@ -0,0 +1,9 @@ +{ + "type": "prerelease", + "area": "feature", + "workstream": "togetherMode", + "comment": "Included a common util method that set call features renderer info", + "packageName": "@azure/communication-react", + "email": "nwankwojustin93@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/calling-stateful-client/src/CallFeatureStreamUtils.ts b/packages/calling-stateful-client/src/CallFeatureStreamUtils.ts index 12bd19efba3..f40aec0591f 100644 --- a/packages/calling-stateful-client/src/CallFeatureStreamUtils.ts +++ b/packages/calling-stateful-client/src/CallFeatureStreamUtils.ts @@ -5,7 +5,12 @@ import { CreateViewOptions, VideoStreamRenderer } from '@azure/communication-cal /* @conditional-compile-remove(together-mode) */ import { CallContext } from './CallContext'; /* @conditional-compile-remove(together-mode) */ -import { CallFeatureStreamState, CreateViewResult } from './index-public'; +import { + CallFeatureStreamName, + CallFeatureStreamState, + CreateViewResult, + VideoStreamRendererViewState +} from './index-public'; /* @conditional-compile-remove(together-mode) */ import { InternalCallContext } from './InternalCallContext'; /* @conditional-compile-remove(together-mode) */ @@ -129,7 +134,7 @@ async function createCallFeatureViewVideo( // and clean up the state. _logStreamEvent(EventNames.RENDER_INFO_NOT_FOUND, streamLogInfo); renderer.dispose(); - context.setTogetherModeVideoStreamRendererView(callId, stream.mediaStreamType, undefined); + setCallFeatureVideoRendererView(callId, featureName, context, stream.mediaStreamType, undefined); return; } @@ -146,7 +151,7 @@ async function createCallFeatureViewVideo( 'NotRendered', undefined ); - context.setTogetherModeVideoStreamRendererView(callId, stream.mediaStreamType, undefined); + setCallFeatureVideoRendererView(callId, featureName, context, stream.mediaStreamType, undefined); return; } @@ -160,8 +165,10 @@ async function createCallFeatureViewVideo( 'Rendered', renderer ); - context.setTogetherModeVideoStreamRendererView( + setCallFeatureVideoRendererView( callId, + featureName, + context, stream.mediaStreamType, convertFromSDKToDeclarativeVideoStreamRendererView(view) ); @@ -211,10 +218,10 @@ function disposeCallFeatureViewVideo( _logStreamEvent(EventNames.START_DISPOSE_STREAM, streamLogInfo); - const featureName = getStreamFeatureName(stream); + const featureName: CallFeatureStreamName = getStreamFeatureName(stream); if (streamEventType === 'disposeViewCallFeature') { - context.setTogetherModeVideoStreamRendererView(callId, streamType, undefined); + setCallFeatureVideoRendererView(callId, featureName, context, streamType, undefined); } const renderInfo = internalContext.getCallFeatureRenderInfo(callId, featureName, stream.mediaStreamType); @@ -265,7 +272,7 @@ function disposeCallFeatureViewVideo( 'NotRendered', undefined ); - context.setTogetherModeVideoStreamRendererView(callId, streamType, undefined); + setCallFeatureVideoRendererView(callId, featureName, context, streamType, undefined); } else { _logStreamEvent(EventNames.RENDERER_NOT_FOUND, streamLogInfo); } @@ -275,7 +282,23 @@ function disposeCallFeatureViewVideo( /** * @private */ -const getStreamFeatureName = (stream: CallFeatureStreamState): string => { +const setCallFeatureVideoRendererView = ( + callId: string, + featureName: CallFeatureStreamName, + context: CallContext, + streamType: string, + view: VideoStreamRendererViewState | undefined +): void => { + if (featureName === 'togetherMode') { + context.setTogetherModeVideoStreamRendererView(callId, streamType, view); + } +}; + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +const getStreamFeatureName = (stream: CallFeatureStreamState): CallFeatureStreamName => { if (stream.feature) { return stream.feature; } From 4abe027e8d453512867293498df58f0cd55c160e Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Thu, 31 Oct 2024 17:18:44 +0000 Subject: [PATCH 03/37] Addressed comments --- .../src/CallClientState.ts | 4 +--- .../calling-stateful-client/src/CallContext.ts | 16 ++++------------ .../src/CallFeatureStreamUtils.ts | 4 ++-- .../calling-stateful-client/src/Converter.ts | 2 +- .../src/StatefulCallClient.ts | 6 +++--- .../src/StreamUtils.test.ts | 2 +- .../calling-stateful-client/src/StreamUtils.ts | 4 ++-- .../src/TogetherModeSubscriber.ts | 4 ++-- .../review/beta/communication-react.api.md | 10 ++++------ .../composites/CallComposite/MockCallAdapter.ts | 2 +- .../CallWithChatComposite/adapter/TestUtils.ts | 2 +- 11 files changed, 22 insertions(+), 34 deletions(-) diff --git a/packages/calling-stateful-client/src/CallClientState.ts b/packages/calling-stateful-client/src/CallClientState.ts index 344e1ce16de..a4126c0783b 100644 --- a/packages/calling-stateful-client/src/CallClientState.ts +++ b/packages/calling-stateful-client/src/CallClientState.ts @@ -292,8 +292,6 @@ export interface CallFeatureStreamState extends RemoteVideoStreamState { * Represents the seating position of a participant in Together Mode. */ export interface TogetherModeSeatingPositionState { - // The participant id of the participant in the seating position. - participantId: string; // The top left offset from the top of the together mode view. top: number; // The left offset position from the left of the together mode view. @@ -329,7 +327,7 @@ export interface TogetherModeCallFeatureState { /** * Proxy of {@link @azure/communication-calling#TogetherModeCallFeature.TogetherModeSeatingMap}. */ - seatingPositions: TogetherModeSeatingPositionState[]; + seatingPositions: Record; } /** diff --git a/packages/calling-stateful-client/src/CallContext.ts b/packages/calling-stateful-client/src/CallContext.ts index bd8b8a12114..ff450a91579 100644 --- a/packages/calling-stateful-client/src/CallContext.ts +++ b/packages/calling-stateful-client/src/CallContext.ts @@ -469,7 +469,7 @@ export class CallContext { if (stream.mediaStreamType === 'Video') { call.togetherMode.streams.mainVideoStream = undefined; call.togetherMode.isActive = false; - call.togetherMode.seatingPositions = []; + call.togetherMode.seatingPositions = {}; } } @@ -553,17 +553,9 @@ export class CallContext { this.modifyState((draft: CallClientState) => { const call = draft.calls[this._callIdHistory.latestCallId(callId)]; if (call) { - const seatingPositions: TogetherModeSeatingPositionState[] = []; - for (const [key, value] of seatingMap.entries()) { - const participantPosition: TogetherModeSeatingPositionState = { - participantId: key, - top: value.top, - left: value.left, - width: value.width, - height: value.height - }; - - seatingPositions.push(participantPosition); + const seatingPositions: Record = {}; + for (const [userId, seatingPosition] of seatingMap.entries()) { + seatingPositions[userId] = seatingPosition; } call.togetherMode.seatingPositions = seatingPositions; } diff --git a/packages/calling-stateful-client/src/CallFeatureStreamUtils.ts b/packages/calling-stateful-client/src/CallFeatureStreamUtils.ts index f40aec0591f..f5a8b559df5 100644 --- a/packages/calling-stateful-client/src/CallFeatureStreamUtils.ts +++ b/packages/calling-stateful-client/src/CallFeatureStreamUtils.ts @@ -25,7 +25,7 @@ import { convertFromSDKToDeclarativeVideoStreamRendererView } from './Converter' * @private * */ -export function createCallFeatureView( +export function createView( context: CallContext, internalContext: InternalCallContext, callId: string | undefined, @@ -184,7 +184,7 @@ async function createCallFeatureViewVideo( /** * @private */ -export function disposeCallFeatureView( +export function disposeView( context: CallContext, internalContext: InternalCallContext, callId: string | undefined, diff --git a/packages/calling-stateful-client/src/Converter.ts b/packages/calling-stateful-client/src/Converter.ts index fa645853b8c..59025054ecd 100644 --- a/packages/calling-stateful-client/src/Converter.ts +++ b/packages/calling-stateful-client/src/Converter.ts @@ -175,7 +175,7 @@ export function convertSdkCallToDeclarativeCall(call: CallCommon): CallState { pptLive: { isActive: false }, raiseHand: { raisedHands: [] }, /* @conditional-compile-remove(together-mode) */ - togetherMode: { isActive: false, streams: {}, seatingPositions: [] }, + togetherMode: { isActive: false, streams: {}, seatingPositions: {} }, localParticipantReaction: undefined, transcription: { isTranscriptionActive: false }, screenShareRemoteParticipant: undefined, diff --git a/packages/calling-stateful-client/src/StatefulCallClient.ts b/packages/calling-stateful-client/src/StatefulCallClient.ts index 59201c69dcb..60721d8f98a 100644 --- a/packages/calling-stateful-client/src/StatefulCallClient.ts +++ b/packages/calling-stateful-client/src/StatefulCallClient.ts @@ -27,7 +27,7 @@ import { DeclarativeTeamsCallAgent, teamsCallAgentDeclaratify } from './TeamsCal import { MicrosoftTeamsUserIdentifier } from '@azure/communication-common'; import { videoStreamRendererViewDeclaratify } from './VideoStreamRendererViewDeclarative'; /* @conditional-compile-remove(together-mode) */ -import { createCallFeatureView, disposeCallFeatureView } from './CallFeatureStreamUtils'; +import { createView as createCallFeatureView, disposeView as disposeCallFeatureView } from './CallFeatureStreamUtils'; /** * Defines the methods that allow CallClient {@link @azure/communication-calling#CallClient} to be used statefully. @@ -166,7 +166,7 @@ export interface StatefulCallClient extends CallClient { * @param options - Options that are passed to the {@link @azure/communication-calling#VideoStreamRenderer}. * @beta */ - createCallFeatureView( + createView( callId: string, stream: CallFeatureStreamState, options?: CreateViewOptions @@ -187,7 +187,7 @@ export interface StatefulCallClient extends CallClient { * @param stream - The LocalVideoStreamState or RemoteVideoStreamState to dispose. * @beta */ - disposeCallFeatureView(callId: string, stream: CallFeatureStreamState): void; + disposeView(callId: string, stream: CallFeatureStreamState): void; /** * The CallAgent is used to handle calls. * To create the CallAgent, pass a CommunicationTokenCredential object provided from SDK. diff --git a/packages/calling-stateful-client/src/StreamUtils.test.ts b/packages/calling-stateful-client/src/StreamUtils.test.ts index 9840e19adb1..2c597a04ee2 100644 --- a/packages/calling-stateful-client/src/StreamUtils.test.ts +++ b/packages/calling-stateful-client/src/StreamUtils.test.ts @@ -88,7 +88,7 @@ function createMockCall(mockCallId: string): CallState { localRecording: { isLocalRecordingActive: false }, raiseHand: { raisedHands: [] }, /* @conditional-compile-remove(together-mode) */ - togetherMode: { isActive: false, streams: {}, seatingPositions: [] }, + togetherMode: { isActive: false, streams: {}, seatingPositions: {} }, localParticipantReaction: undefined, transcription: { isTranscriptionActive: false }, screenShareRemoteParticipant: undefined, diff --git a/packages/calling-stateful-client/src/StreamUtils.ts b/packages/calling-stateful-client/src/StreamUtils.ts index 4ca1296e0b6..90e6514a1fa 100644 --- a/packages/calling-stateful-client/src/StreamUtils.ts +++ b/packages/calling-stateful-client/src/StreamUtils.ts @@ -582,8 +582,8 @@ export function disposeAllViewsFromCall( const callFeatureStreams = internalContext.getCallFeatureRenderInfosForCall(callId); /* @conditional-compile-remove(together-mode) */ if (callFeatureStreams) { - for (const [, featureStreams] of callFeatureStreams.entries()) { - for (const [, streamAndRenderer] of featureStreams.entries()) { + for (const featureStreams of callFeatureStreams.values()) { + for (const streamAndRenderer of featureStreams.values()) { disposeView( context, internalContext, diff --git a/packages/calling-stateful-client/src/TogetherModeSubscriber.ts b/packages/calling-stateful-client/src/TogetherModeSubscriber.ts index ab9e956d130..1982c8ce631 100644 --- a/packages/calling-stateful-client/src/TogetherModeSubscriber.ts +++ b/packages/calling-stateful-client/src/TogetherModeSubscriber.ts @@ -15,7 +15,7 @@ import { CallIdRef } from './CallIdRef'; /* @conditional-compile-remove(together-mode) */ import { InternalCallContext } from './InternalCallContext'; /* @conditional-compile-remove(together-mode) */ -import { disposeCallFeatureView } from './CallFeatureStreamUtils'; +import { disposeView } from './CallFeatureStreamUtils'; /* @conditional-compile-remove(together-mode) */ import { convertSdkCallFeatureStreamToDeclarativeCallFeatureStream } from './Converter'; /* @conditional-compile-remove(together-mode) */ @@ -82,7 +82,7 @@ export class TogetherModeSubscriber { ): void => { for (const stream of removedStreams) { this._togetherModeVideoStreamSubscribers.get(stream.id)?.unsubscribe(); - disposeCallFeatureView( + disposeView( this._context, this._internalContext, this._callIdRef.callId, diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index 834697f035e..447670894df 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -4775,13 +4775,13 @@ export type StartTeamsCallIdentifier = MicrosoftTeamsUserIdentifier | PhoneNumbe // @public export interface StatefulCallClient extends CallClient { createCallAgent(...args: Parameters): Promise; - // @beta - createCallFeatureView(callId: string, stream: CallFeatureStreamState, options?: CreateViewOptions): Promise; createTeamsCallAgent(...args: Parameters): Promise; createView(callId: string | undefined, participantId: CommunicationIdentifier | undefined, stream: LocalVideoStreamState | RemoteVideoStreamState, options?: CreateViewOptions): Promise; // @beta - disposeCallFeatureView(callId: string, stream: CallFeatureStreamState): void; + createView(callId: string, stream: CallFeatureStreamState, options?: CreateViewOptions): Promise; disposeView(callId: string | undefined, participantId: CommunicationIdentifier | undefined, stream: LocalVideoStreamState | RemoteVideoStreamState): void; + // @beta + disposeView(callId: string, stream: CallFeatureStreamState): void; getState(): CallClientState; offStateChange(handler: (state: CallClientState) => void): void; onStateChange(handler: (state: CallClientState) => void): void; @@ -4970,7 +4970,7 @@ export const toFlatCommunicationIdentifier: (identifier: CommunicationIdentifier export interface TogetherModeCallFeature { // (undocumented) isActive: boolean; - seatingPositions: TogetherModeSeatingPositionState[]; + seatingPositions: Record; streams: TogetherModeStreamsState; } @@ -4981,8 +4981,6 @@ export interface TogetherModeSeatingPositionState { // (undocumented) left: number; // (undocumented) - participantId: string; - // (undocumented) top: number; // (undocumented) width: number; diff --git a/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts b/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts index 288c26e7e79..c0b3220c9dd 100644 --- a/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts @@ -270,7 +270,7 @@ const createDefaultCallAdapterState = (role?: ParticipantRole): CallAdapterState remoteParticipantsEnded: {}, raiseHand: { raisedHands: [] }, /* @conditional-compile-remove(together-mode) */ - togetherMode: { isActive: false, streams: {}, seatingPositions: [] }, + togetherMode: { isActive: false, streams: {}, seatingPositions: {} }, pptLive: { isActive: false }, localParticipantReaction: undefined, role, diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/TestUtils.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/TestUtils.ts index 79cb9a2c15b..63e68c32760 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/TestUtils.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/TestUtils.ts @@ -240,7 +240,7 @@ function createMockCall(mockCallId: string): CallState { dominantSpeakers: undefined, raiseHand: { raisedHands: [] }, /* @conditional-compile-remove(together-mode) */ - togetherMode: { isActive: false, streams: {}, seatingPositions: [] }, + togetherMode: { isActive: false, streams: {}, seatingPositions: {} }, pptLive: { isActive: false }, localParticipantReaction: undefined, captionsFeature: { From 592cff13eff65e795c6a0cc00c030a632e267b6d Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Fri, 1 Nov 2024 20:01:33 +0000 Subject: [PATCH 04/37] Together Mode APIs --- ...-c495d5e5-3d35-4714-a329-a839d316e765.json | 9 ++ .../src/handlers/createCommonHandlers.ts | 94 ++++++++++++++++++- .../src/handlers/createTeamsCallHandlers.ts | 16 ++++ .../review/beta/communication-react.api.md | 30 ++++++ packages/communication-react/src/index.ts | 3 + packages/react-components/src/index.ts | 3 + .../src/types/TogetherModeTypes.ts | 56 +++++++++++ packages/react-components/src/types/index.ts | 2 + .../adapter/AzureCommunicationCallAdapter.ts | 33 +++++++ .../CallComposite/adapter/CallAdapter.ts | 51 +++++++++- .../CallComposite/hooks/useHandlers.ts | 16 ++++ .../AzureCommunicationCallWithChatAdapter.ts | 20 ++++ .../adapter/CallWithChatAdapter.ts | 41 +++++++- .../adapter/CallWithChatBackedCallAdapter.ts | 15 +++ 14 files changed, 385 insertions(+), 4 deletions(-) create mode 100644 change-beta/@azure-communication-react-c495d5e5-3d35-4714-a329-a839d316e765.json create mode 100644 packages/react-components/src/types/TogetherModeTypes.ts diff --git a/change-beta/@azure-communication-react-c495d5e5-3d35-4714-a329-a839d316e765.json b/change-beta/@azure-communication-react-c495d5e5-3d35-4714-a329-a839d316e765.json new file mode 100644 index 00000000000..b77065e09ac --- /dev/null +++ b/change-beta/@azure-communication-react-c495d5e5-3d35-4714-a329-a839d316e765.json @@ -0,0 +1,9 @@ +{ + "type": "prerelease", + "area": "feature", + "workstream": "togetherMode", + "comment": "Together Mode APIs", + "packageName": "@azure/communication-react", + "email": "nwankwojustin93@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts index f84e2f950ab..ce6993dcf7b 100644 --- a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts +++ b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts @@ -40,7 +40,8 @@ import { Features } from '@azure/communication-calling'; import { TeamsCaptions } from '@azure/communication-calling'; import { Reaction } from '@azure/communication-calling'; import { _ComponentCallingHandlers } from './createHandlers'; - +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeStreamViewResult } from '@internal/react-components'; /** * Object containing all the handlers required for calling components. * @@ -103,6 +104,35 @@ export interface CommonCallingHandlers { onStopAllSpotlight: () => Promise; onMuteParticipant: (userId: string) => Promise; onMuteAllRemoteParticipants: () => Promise; + /* @conditional-compile-remove(together-mode) */ + /** + * Call back to create a view for together mode + * + * @beta + */ + onCreateTogetherModeStreamView: (options?: VideoStreamOptions) => Promise; + + /* @conditional-compile-remove(together-mode) */ + /** + * Call back to create a view for together mode + * + * @beta + */ + onStartTogetherMode: () => Promise; + /* @conditional-compile-remove(together-mode) */ + /** + * Call set together mode scene size + * + * @beta + */ + onSetTogetherModeSceneSize: (width: number, height: number) => void; + /* @conditional-compile-remove(together-mode) */ + /** + * Call back to dispose together mode views + * + * @beta + */ + onDisposeTogetherModeStreamViews: () => Promise; } /** @@ -711,7 +741,59 @@ export const createDefaultCommonCallingHandlers = memoizeOne( await call?.feature(Features.Spotlight).stopSpotlight(participants); } : undefined; + /* @conditional-compile-remove(together-mode) */ + const onCreateTogetherModeStreamView = async ( + options = { scalingMode: 'Fit', isMirrored: false } as VideoStreamOptions + ): Promise => { + if (!call) { + return; + } + const callState = callClient.getState().calls[call.id]; + if (!callState) { + return; + } + const togetherModeStreams = callState.togetherMode.streams; + const togetherModeCreateViewResult: TogetherModeStreamViewResult = {}; + + const mainVideoStream = togetherModeStreams.mainVideoStream; + if (mainVideoStream && mainVideoStream.isAvailable && !mainVideoStream.view) { + const createViewResult = await callClient.createView(call.id, mainVideoStream, options); + // SDK currently only supports 1 Video media stream type + togetherModeCreateViewResult.mainVideoView = createViewResult?.view + ? { view: createViewResult?.view } + : undefined; + } + + return togetherModeCreateViewResult; + }; + + /* @conditional-compile-remove(together-mode) */ + const onDisposeTogetherModeStreamViews = async (): Promise => { + if (!call) { + return; + } + const callState = callClient.getState().calls[call.id]; + if (!callState) { + throw new Error(`Call Not Found: ${call.id}`); + } + + const togetherModeStreams = callState.togetherMode.streams; + + if (!togetherModeStreams.mainVideoStream) { + return; + } + if (togetherModeStreams.mainVideoStream.view) { + callClient.disposeView(call.id, togetherModeStreams.mainVideoStream); + } + }; + /* @conditional-compile-remove(together-mode) */ + const onSetTogetherModeSceneSize = (width: number, height: number): void => { + const togetherModeFeature = call?.feature(Features.TogetherMode); + if (togetherModeFeature) { + togetherModeFeature.sceneSize = { width, height }; + } + }; return { onHangUp, onToggleHold, @@ -761,7 +843,15 @@ export const createDefaultCommonCallingHandlers = memoizeOne( onMuteParticipant, onMuteAllRemoteParticipants, onAcceptCall: notImplemented, - onRejectCall: notImplemented + onRejectCall: notImplemented, + /* @conditional-compile-remove(together-mode) */ + onCreateTogetherModeStreamView, + /* @conditional-compile-remove(together-mode) */ + onStartTogetherMode: notImplemented, + /* @conditional-compile-remove(together-mode) */ + onSetTogetherModeSceneSize, + /* @conditional-compile-remove(together-mode) */ + onDisposeTogetherModeStreamViews }; } ); diff --git a/packages/calling-component-bindings/src/handlers/createTeamsCallHandlers.ts b/packages/calling-component-bindings/src/handlers/createTeamsCallHandlers.ts index f01919fe2f4..7197392dd66 100644 --- a/packages/calling-component-bindings/src/handlers/createTeamsCallHandlers.ts +++ b/packages/calling-component-bindings/src/handlers/createTeamsCallHandlers.ts @@ -2,6 +2,8 @@ // Licensed under the MIT License. import { StartCallOptions } from '@azure/communication-calling'; +/* @conditional-compile-remove(together-mode) */ +import { Features } from '@azure/communication-calling'; import { IncomingCallCommon } from '@azure/communication-calling'; /* @conditional-compile-remove(teams-identity-support-beta) */ import { AddPhoneNumberOptions } from '@azure/communication-calling'; @@ -125,6 +127,20 @@ export const createDefaultTeamsCallingHandlers = memoizeOne( if (incomingCall) { await incomingCall.reject(); } + }, + /* @conditional-compile-remove(together-mode) */ + onStartTogetherMode: async (): Promise => { + if (!call) { + return; + } + const callState = callClient.getState().calls[call.id]; + if (!callState) { + return; + } + if (!callState.togetherMode.isActive) { + const togetherModeFeature = call?.feature(Features.TogetherMode); + await togetherModeFeature?.start(); + } } }; } diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index 447670894df..02bd05f914b 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -443,11 +443,15 @@ export interface CallAdapterCallOperations { addParticipant(participant: CommunicationUserIdentifier): Promise; allowUnsupportedBrowserVersion(): void; createStreamView(remoteUserId?: string, options?: VideoStreamOptions): Promise; + // @beta + createTogetherModeStreamViews(options?: VideoStreamOptions): Promise; disposeLocalVideoStreamView(): Promise; disposeRemoteVideoStreamView(remoteUserId: string): Promise; disposeScreenShareStreamView(remoteUserId: string): Promise; // @deprecated disposeStreamView(remoteUserId?: string, options?: VideoStreamOptions): Promise; + // @beta + disposeTogetherModeStreamViews(): Promise; holdCall(): Promise; leaveCall(forEveryone?: boolean): Promise; lowerHand(): Promise; @@ -463,11 +467,15 @@ export interface CallAdapterCallOperations { sendDtmfTone(dtmfTone: DtmfTone_2): Promise; setCaptionLanguage(language: string): Promise; setSpokenLanguage(language: string): Promise; + // @beta + setTogetherModeSceneSize(width: number, height: number): void; startCamera(options?: VideoStreamOptions): Promise; startCaptions(options?: StartCaptionsAdapterOptions): Promise; startNoiseSuppressionEffect(): Promise; startScreenShare(): Promise; startSpotlight(userIds?: string[]): Promise; + // @beta + startTogetherMode(): Promise; startVideoBackgroundEffect(videoBackgroundEffect: VideoBackgroundEffect): Promise; stopAllSpotlight(): Promise; stopCamera(): Promise; @@ -1206,12 +1214,16 @@ export interface CallWithChatAdapterManagement { askDevicePermission(constrain: PermissionConstraints): Promise; createStreamView(remoteUserId?: string, options?: VideoStreamOptions): Promise; // @beta + createTogetherModeStreamViews(options?: VideoStreamOptions): Promise; + // @beta deleteImage(imageId: string): Promise; deleteMessage(messageId: string): Promise; disposeLocalVideoStreamView(): Promise; disposeRemoteVideoStreamView(remoteUserId: string): Promise; disposeScreenShareStreamView(remoteUserId: string): Promise; disposeStreamView(remoteUserId?: string, options?: VideoStreamOptions): Promise; + // @beta + disposeTogetherModeStreamViews(): Promise; // (undocumented) downloadResourceToCache(resourceDetails: ResourceDetails): Promise; fetchInitialData(): Promise; @@ -1245,6 +1257,8 @@ export interface CallWithChatAdapterManagement { setMicrophone(sourceInfo: AudioDeviceInfo): Promise; setSpeaker(sourceInfo: AudioDeviceInfo): Promise; setSpokenLanguage(language: string): Promise; + // @beta + setTogetherModeSceneSize(width: number, height: number): void; startCall(participants: string[], options?: StartCallOptions): Call | undefined; startCall(participants: (MicrosoftTeamsAppIdentifier | PhoneNumberIdentifier | CommunicationUserIdentifier | MicrosoftTeamsUserIdentifier | UnknownIdentifier)[], options?: StartCallOptions): Call | undefined; startCamera(options?: VideoStreamOptions): Promise; @@ -1252,6 +1266,8 @@ export interface CallWithChatAdapterManagement { startNoiseSuppressionEffect(): Promise; startScreenShare(): Promise; startSpotlight(userIds?: string[]): Promise; + // (undocumented) + startTogetherMode(): Promise; startVideoBackgroundEffect(videoBackgroundEffect: VideoBackgroundEffect): Promise; stopAllSpotlight(): Promise; stopCamera(): Promise; @@ -2184,6 +2200,8 @@ export interface CommonCallingHandlers { onCreateLocalStreamView: (options?: VideoStreamOptions) => Promise; // (undocumented) onCreateRemoteStreamView: (userId: string, options?: VideoStreamOptions) => Promise; + // @beta + onCreateTogetherModeStreamView: (options?: VideoStreamOptions) => Promise; // (undocumented) onDisposeLocalScreenShareStreamView: () => Promise; // (undocumented) @@ -2194,6 +2212,8 @@ export interface CommonCallingHandlers { onDisposeRemoteStreamView: (userId: string) => Promise; // (undocumented) onDisposeRemoteVideoStreamView: (userId: string) => Promise; + // @beta + onDisposeTogetherModeStreamViews: () => Promise; // (undocumented) onHangUp: (forEveryone?: boolean) => Promise; // (undocumented) @@ -2228,6 +2248,8 @@ export interface CommonCallingHandlers { onSetCaptionLanguage: (language: string) => Promise; // (undocumented) onSetSpokenLanguage: (language: string) => Promise; + // @beta + onSetTogetherModeSceneSize: (width: number, height: number) => void; // (undocumented) onStartCall: (participants: CommunicationIdentifier[], options?: StartCallOptions) => void; // (undocumented) @@ -2240,6 +2262,8 @@ export interface CommonCallingHandlers { onStartScreenShare: () => Promise; // (undocumented) onStartSpotlight: (userIds?: string[]) => Promise; + // @beta + onStartTogetherMode: () => Promise; // (undocumented) onStopAllSpotlight: () => Promise; // (undocumented) @@ -4992,6 +5016,12 @@ export interface TogetherModeStreamsState { mainVideoStream?: CallFeatureStreamState; } +// @beta +export interface TogetherModeStreamViewResult { + // (undocumented) + mainVideoView?: CreateVideoStreamViewResult; +} + // @public export type TopicChangedListener = (event: { topic: string; diff --git a/packages/communication-react/src/index.ts b/packages/communication-react/src/index.ts index c49b2fc7654..89c4477c335 100644 --- a/packages/communication-react/src/index.ts +++ b/packages/communication-react/src/index.ts @@ -311,6 +311,9 @@ export type { VideoTilesOptions } from '../../react-components/src'; +/* @conditional-compile-remove(together-mode) */ +export type { TogetherModeStreamViewResult } from '../../react-components/src'; + export type { RaiseHandButtonProps, RaiseHandButtonStrings, RaisedHand } from '../../react-components/src'; export type { ReactionButtonStrings, diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index c29a9d332a6..4113d086a37 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -84,3 +84,6 @@ export type { SurveyIssues } from './types'; export type { SurveyIssuesHeadingStrings } from './types'; export type { CallSurveyImprovementSuggestions } from './types'; + +/* @conditional-compile-remove(together-mode) */ +export type { TogetherModeStreamViewResult } from './types'; diff --git a/packages/react-components/src/types/TogetherModeTypes.ts b/packages/react-components/src/types/TogetherModeTypes.ts new file mode 100644 index 00000000000..96c60a680c9 --- /dev/null +++ b/packages/react-components/src/types/TogetherModeTypes.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* @conditional-compile-remove(together-mode) */ +import { CreateVideoStreamViewResult } from './VideoGalleryParticipant'; + +/* @conditional-compile-remove(together-mode) */ +/** + * Interface representing the result of a Together Mode stream view. + * @beta + */ +export interface TogetherModeStreamViewResult { + mainVideoView?: CreateVideoStreamViewResult; +} + +/* @conditional-compile-remove(together-mode) */ +/** + * Represents a video stream in Together Mode. + * @beta + */ +export interface TogetherModeVideoStream { + isAvailable?: boolean; + /** + * The HTML element used to render the video stream. + * + */ + renderElement?: HTMLElement; + + /** + * The size of the video stream. + * @optional + */ + streamSize?: { width: number; height: number }; +} + +/* @conditional-compile-remove(together-mode) */ +/** + * Interface representing the streams in Together Mode. + * @beta + */ +export interface TogetherModeStreams { + mainVideoStream?: TogetherModeVideoStream; +} + +/* @conditional-compile-remove(together-mode) */ +/** + * Interface representing the position of a participant in Together Mode. + * @beta + */ +export interface TogetherModeParticipantPosition { + participantId: string; + top: number; + left: number; + width: number; + height: number; +} diff --git a/packages/react-components/src/types/index.ts b/packages/react-components/src/types/index.ts index 07b84e17660..429399f112b 100644 --- a/packages/react-components/src/types/index.ts +++ b/packages/react-components/src/types/index.ts @@ -15,3 +15,5 @@ export * from './SurveyIssuesHeadingStrings'; export * from './CallSurveyImprovementSuggestions'; export * from './ReactionTypes'; export * from './Attachment'; +/* @conditional-compile-remove(together-mode) */ +export * from './TogetherModeTypes'; diff --git a/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts b/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts index d6d87ef5711..c7c78a8c438 100644 --- a/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts @@ -116,6 +116,9 @@ import { import { CallSurvey, CallSurveyResponse } from '@azure/communication-calling'; import { CallingSoundSubscriber } from './CallingSoundSubscriber'; import { CallingSounds } from './CallAdapter'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeStreamViewResult } from '@internal/react-components'; + type CallTypeOf = AgentType extends CallAgent ? Call : TeamsCall; /** @@ -596,6 +599,14 @@ export class AzureCommunicationCallAdapter { + return await this.handlers.onCreateTogetherModeStreamView(options); + } + + /* @conditional-compile-remove(together-mode) */ + public async startTogetherMode(): Promise { + return await this.handlers.onStartTogetherMode(); + } + + /* @conditional-compile-remove(together-mode) */ + public setTogetherModeSceneSize(width: number, height: number): void { + return this.handlers.onSetTogetherModeSceneSize(width, height); + } + + /* @conditional-compile-remove(together-mode) */ + public async disposeTogetherModeStreamViews(): Promise { + return await this.handlers.onDisposeTogetherModeStreamViews(); + } + public async leaveCall(forEveryone?: boolean): Promise { if (this.getState().page === 'transferring') { const transferCall = this.callAgent.calls.filter( diff --git a/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts b/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts index c3e4d980a08..410a7de29af 100644 --- a/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts @@ -39,7 +39,8 @@ import { } from '@internal/calling-component-bindings'; import { CallSurvey, CallSurveyResponse } from '@azure/communication-calling'; import { ReactionResources } from '@internal/react-components'; - +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeStreamViewResult } from '@internal/react-components'; /** * Major UI screens shown in the {@link CallComposite}. * @@ -609,6 +610,54 @@ export interface CallAdapterCallOperations { * @public */ disposeStreamView(remoteUserId?: string, options?: VideoStreamOptions): Promise; + /* @conditional-compile-remove(together-mode) */ + /** + * Create the html view for a stream. + * + * @remarks + * This method is implemented for composite + * + * @param featureName - Name of feature to render + * @param options - Options to control how video streams are rendered {@link @azure/communication-calling#VideoStreamOptions } + * + * @beta + */ + createTogetherModeStreamViews(options?: VideoStreamOptions): Promise; + /* @conditional-compile-remove(together-mode) */ + /** + * Start Together mode. + * + * @beta + */ + startTogetherMode(): Promise; + /* @conditional-compile-remove(together-mode) */ + /** + * Recalculate the seating positions for together mode. + * + * @remarks + * This method is implemented for composite + * + * @param width - Width of the container + * @param height - Height of the container + * + * @beta + */ + setTogetherModeSceneSize(width: number, height: number): void; + + /* @conditional-compile-remove(together-mode) */ + /** + * Dispose the html view for a stream. + * + * @remarks + * This method is implemented for composite + * + * + * @param featureName - Name of the feature to dispose + * @param options - Options to control how video streams are rendered {@link @azure/communication-calling#VideoStreamOptions } + * + * @beta + */ + disposeTogetherModeStreamViews(): Promise; /** * Dispose the html view for a screen share stream * diff --git a/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts b/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts index e5baab982ca..4ce0a1ccb5a 100644 --- a/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts +++ b/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts @@ -228,6 +228,22 @@ const createCompositeHandlers = memoizeOne( }, onMuteAllRemoteParticipants: async (): Promise => { await adapter.muteAllRemoteParticipants(); + }, + /* @conditional-compile-remove(together-mode) */ + onCreateTogetherModeStreamView: async (options) => { + return await adapter.createTogetherModeStreamViews(options); + }, + /* @conditional-compile-remove(together-mode) */ + onStartTogetherMode: async () => { + return await adapter.startTogetherMode(); + }, + /* @conditional-compile-remove(together-mode) */ + onSetTogetherModeSceneSize: (width: number, height: number) => { + return adapter.setTogetherModeSceneSize(width, height); + }, + /* @conditional-compile-remove(together-mode) */ + onDisposeTogetherModeStreamViews: async () => { + return await adapter.disposeTogetherModeStreamViews(); } }; } diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts index d992339c041..cbf7bdfd592 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts @@ -26,6 +26,8 @@ import { CreateVideoStreamViewResult, VideoStreamOptions } from '@internal/react import { MessageOptions } from '@internal/acs-ui-common'; /* @conditional-compile-remove(breakout-rooms) */ import { toFlatCommunicationIdentifier } from '@internal/acs-ui-common'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeStreamViewResult } from '@internal/react-components'; import { ParticipantsJoinedListener, ParticipantsLeftListener, @@ -518,6 +520,24 @@ export class AzureCommunicationCallWithChatAdapter implements CallWithChatAdapte public async disposeLocalVideoStreamView(): Promise { await this.callAdapter.disposeLocalVideoStreamView(); } + /* @conditional-compile-remove(together-mode) */ + public async createTogetherModeStreamViews( + options?: VideoStreamOptions + ): Promise { + return await this.callAdapter.createTogetherModeStreamViews(options); + } + /* @conditional-compile-remove(together-mode) */ + public async startTogetherMode(): Promise { + return await this.callAdapter.startTogetherMode(); + } + /* @conditional-compile-remove(together-mode) */ + public setTogetherModeSceneSize(width: number, height: number): void { + return this.callAdapter.setTogetherModeSceneSize(width, height); + } + /* @conditional-compile-remove(together-mode) */ + public async disposeTogetherModeStreamViews(): Promise { + await this.callAdapter.disposeTogetherModeStreamViews(); + } /** Fetch initial Call and Chat data such as chat messages. */ public async fetchInitialData(): Promise { return await this.executeWithResolvedChatAdapter((adapter) => { diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts index e11aa98927a..6b9b634aa5d 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts @@ -41,6 +41,8 @@ import { AddPhoneNumberOptions } from '@azure/communication-calling'; import { BreakoutRoomsUpdatedListener } from '@azure/communication-calling'; import { DtmfTone } from '@azure/communication-calling'; import { CreateVideoStreamViewResult, VideoStreamOptions } from '@internal/react-components'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeStreamViewResult } from '@internal/react-components'; import { SendMessageOptions } from '@azure/communication-chat'; /* @conditional-compile-remove(rich-text-editor-image-upload) */ import { UploadChatImageResult } from '@internal/acs-ui-common'; @@ -69,7 +71,6 @@ import { SpotlightChangedListener } from '../../CallComposite/adapter/CallAdapte import { VideoBackgroundImage, VideoBackgroundEffect } from '../../CallComposite'; import { CallSurvey, CallSurveyResponse } from '@azure/communication-calling'; - /** * Functionality for managing the current call with chat. * @public @@ -227,6 +228,44 @@ export interface CallWithChatAdapterManagement { * @public */ disposeStreamView(remoteUserId?: string, options?: VideoStreamOptions): Promise; + /* @conditional-compile-remove(together-mode) */ + /** + * Create the html view for a stream. + * + * @remarks + * This method is implemented for composite + * + * @param options - Options to control how video streams are rendered {@link @azure/communication-calling#VideoStreamOptions } + * + * @beta + */ + createTogetherModeStreamViews(options?: VideoStreamOptions): Promise; + /* @conditional-compile-remove(together-mode) */ + startTogetherMode(): Promise; + /* @conditional-compile-remove(together-mode) */ + /** + * Recalculate the seating positions for together mode. + * + * @remarks + * This method is implemented for composite + * + * @param width - Width of the container + * @param height - Height of the container + * + * @beta + */ + setTogetherModeSceneSize(width: number, height: number): void; + + /* @conditional-compile-remove(together-mode) */ + /** + * Dispose the html view for a stream. + * + * @remarks + * This method is implemented for composite + * + * @beta + */ + disposeTogetherModeStreamViews(): Promise; /** * Dispose the html view for a screen share stream * diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts index e25f049c4e1..fa078b0b6a4 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts @@ -6,6 +6,8 @@ import { CallAdapter, CallAdapterState } from '../../CallComposite'; import { VideoBackgroundImage, VideoBackgroundEffect } from '../../CallComposite'; import { CreateVideoStreamViewResult, VideoStreamOptions } from '@internal/react-components'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeStreamViewResult } from '@internal/react-components'; import { AudioDeviceInfo, VideoDeviceInfo, @@ -135,6 +137,16 @@ export class CallWithChatBackedCallAdapter implements CallAdapter { options?: VideoStreamOptions ): Promise => await this.callWithChatAdapter.createStreamView(remoteUserId, options); + /* @conditional-compile-remove(together-mode) */ + public createTogetherModeStreamViews = async ( + options?: VideoStreamOptions + ): Promise => + await this.callWithChatAdapter.createTogetherModeStreamViews(options); + /* @conditional-compile-remove(together-mode) */ + public startTogetherMode = async (): Promise => await this.callWithChatAdapter.startTogetherMode(); + /* @conditional-compile-remove(together-mode) */ + public setTogetherModeSceneSize = (width: number, height: number): void => + this.callWithChatAdapter.setTogetherModeSceneSize(width, height); public disposeStreamView = async (remoteUserId?: string, options?: VideoStreamOptions): Promise => await this.callWithChatAdapter.disposeStreamView(remoteUserId, options); public disposeScreenShareStreamView(remoteUserId: string): Promise { @@ -146,6 +158,9 @@ export class CallWithChatBackedCallAdapter implements CallAdapter { public disposeLocalVideoStreamView(): Promise { return this.callWithChatAdapter.disposeLocalVideoStreamView(); } + /* @conditional-compile-remove(together-mode) */ + public disposeTogetherModeStreamViews = async (): Promise => + await this.callWithChatAdapter.disposeTogetherModeStreamViews(); public holdCall = async (): Promise => { await this.callWithChatAdapter.holdCall(); }; From aa77cda1cf4a00d47dd6d8dc8462aef00216f0ec Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Fri, 1 Nov 2024 15:08:34 -0500 Subject: [PATCH 05/37] Update @azure-communication-react-c495d5e5-3d35-4714-a329-a839d316e765.json Signed-off-by: Chukwuebuka Nwankwo --- ...communication-react-c495d5e5-3d35-4714-a329-a839d316e765.json | 1 - 1 file changed, 1 deletion(-) diff --git a/change-beta/@azure-communication-react-c495d5e5-3d35-4714-a329-a839d316e765.json b/change-beta/@azure-communication-react-c495d5e5-3d35-4714-a329-a839d316e765.json index b77065e09ac..80b39a5ba0f 100644 --- a/change-beta/@azure-communication-react-c495d5e5-3d35-4714-a329-a839d316e765.json +++ b/change-beta/@azure-communication-react-c495d5e5-3d35-4714-a329-a839d316e765.json @@ -4,6 +4,5 @@ "workstream": "togetherMode", "comment": "Together Mode APIs", "packageName": "@azure/communication-react", - "email": "nwankwojustin93@gmail.com", "dependentChangeType": "patch" } From 2a3a676cd420c587ccf046616cbcefd1370e5e1c Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Sun, 3 Nov 2024 07:49:12 +0000 Subject: [PATCH 06/37] TogetherModeTypes interface update --- .../react-components/src/types/TogetherModeTypes.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/react-components/src/types/TogetherModeTypes.ts b/packages/react-components/src/types/TogetherModeTypes.ts index 96c60a680c9..4307a3777a3 100644 --- a/packages/react-components/src/types/TogetherModeTypes.ts +++ b/packages/react-components/src/types/TogetherModeTypes.ts @@ -42,15 +42,20 @@ export interface TogetherModeStreams { mainVideoStream?: TogetherModeVideoStream; } -/* @conditional-compile-remove(together-mode) */ /** - * Interface representing the position of a participant in Together Mode. + * Interface representing the seating information in Together Mode. * @beta */ -export interface TogetherModeParticipantPosition { - participantId: string; +export interface TogetherModeSeatingInfo { top: number; left: number; width: number; height: number; } + +/* @conditional-compile-remove(together-mode) */ +/** + * Interface representing the position of a participant in Together Mode. + * @beta + */ +export type TogetherModeParticipantPosition = Record; From e3220c789b2e0127cd86f49d0531dd714da50786 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Mon, 4 Nov 2024 19:54:31 +0000 Subject: [PATCH 07/37] Together Mode Stream Implementation --- ...-8e6ff7e4-7b2d-4f78-9444-354cc96f9985.json | 8 + .../src/baseSelectors.ts | 11 + .../src/videoGallerySelector.ts | 41 ++- .../src/CallClientState.ts | 10 +- .../src/index-public.ts | 1 + .../review/beta/communication-react.api.md | 61 +++- .../review/stable/communication-react.api.md | 3 +- packages/communication-react/src/index.ts | 8 +- .../src/components/MeetingReactionOverlay.tsx | 20 ++ .../src/components/TogetherModeOverlay.tsx | 277 ++++++++++++++++++ .../src/components/VideoGallery.tsx | 103 ++++++- .../src/components/VideoGallery/Layout.ts | 10 + .../VideoGallery/TogetherModeLayout.tsx | 20 ++ .../VideoGallery/TogetherModeStream.tsx | 116 ++++++++ .../styles/ReactionOverlay.style.ts | 13 + packages/react-components/src/index.ts | 12 +- .../src/types/ReactionTypes.ts | 2 +- .../src/types/TogetherModeTypes.ts | 25 +- .../src/composites/CallComposite/Strings.tsx | 5 + .../CallComposite/components/MediaGallery.tsx | 7 +- .../CallComposite/selectors/baseSelectors.ts | 10 + .../common/ControlBar/DesktopMoreButton.tsx | 29 ++ .../localization/locales/en-US/strings.json | 4 +- 23 files changed, 765 insertions(+), 31 deletions(-) create mode 100644 change-beta/@azure-communication-react-8e6ff7e4-7b2d-4f78-9444-354cc96f9985.json create mode 100644 packages/react-components/src/components/TogetherModeOverlay.tsx create mode 100644 packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx create mode 100644 packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx diff --git a/change-beta/@azure-communication-react-8e6ff7e4-7b2d-4f78-9444-354cc96f9985.json b/change-beta/@azure-communication-react-8e6ff7e4-7b2d-4f78-9444-354cc96f9985.json new file mode 100644 index 00000000000..295edde5e0c --- /dev/null +++ b/change-beta/@azure-communication-react-8e6ff7e4-7b2d-4f78-9444-354cc96f9985.json @@ -0,0 +1,8 @@ +{ + "type": "prerelease", + "area": "feature", + "workstream": "togetherMode", + "comment": "TogetherMode Stream view implementation", + "packageName": "@azure/communication-react", + "dependentChangeType": "patch" +} diff --git a/packages/calling-component-bindings/src/baseSelectors.ts b/packages/calling-component-bindings/src/baseSelectors.ts index c0c5f9dd9a4..e18abfdbf07 100644 --- a/packages/calling-component-bindings/src/baseSelectors.ts +++ b/packages/calling-component-bindings/src/baseSelectors.ts @@ -26,6 +26,8 @@ import { _SupportedCaptionLanguage, _SupportedSpokenLanguage } from '@internal/r import { ConferencePhoneInfo } from '@internal/calling-stateful-client'; /* @conditional-compile-remove(breakout-rooms) */ import { CallNotifications } from '@internal/calling-stateful-client'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeCallFeatureState } from '@internal/calling-stateful-client/dist/dist-esm/CallClientState'; /** * Common props used to reference calling declarative client state. @@ -293,3 +295,12 @@ export const getAssignedBreakoutRoom = ( ): BreakoutRoom | undefined => { return state.calls[props.callId]?.breakoutRooms?.assignedBreakoutRoom; }; + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +export const getTogetherModeCallFeature = ( + state: CallClientState, + props: CallingBaseSelectorProps +): TogetherModeCallFeatureState | undefined => state.calls[props.callId]?.togetherMode; diff --git a/packages/calling-component-bindings/src/videoGallerySelector.ts b/packages/calling-component-bindings/src/videoGallerySelector.ts index 1e2406a94f1..ebbd56f1154 100644 --- a/packages/calling-component-bindings/src/videoGallerySelector.ts +++ b/packages/calling-component-bindings/src/videoGallerySelector.ts @@ -3,7 +3,11 @@ import { toFlatCommunicationIdentifier } from '@internal/acs-ui-common'; import { CallClientState, RemoteParticipantState } from '@internal/calling-stateful-client'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeParticipantSeatingState, TogetherModeStreamsState } from '@internal/calling-stateful-client'; import { VideoGalleryRemoteParticipant, VideoGalleryLocalParticipant } from '@internal/react-components'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeStreamsProp } from '@internal/react-components'; import { createSelector } from 'reselect'; import { CallingBaseSelectorProps, @@ -16,6 +20,8 @@ import { getRole, getScreenShareRemoteParticipant } from './baseSelectors'; +/* @conditional-compile-remove(together-mode) */ +import { getTogetherModeCallFeature } from './baseSelectors'; import { isHideAttendeeNamesEnabled } from './baseSelectors'; import { getOptimalVideoCount } from './baseSelectors'; import { _updateUserDisplayNames } from './utils/callUtils'; @@ -49,6 +55,14 @@ export type VideoGallerySelector = ( optimalVideoCount?: number; spotlightedParticipants?: string[]; maxParticipantsToSpotlight?: number; + /* @conditional-compile-remove(together-mode) */ + isTogetherModeActive?: boolean; + /* @conditional-compile-remove(together-mode) */ + canStartTogetherMode?: boolean; + /* @conditional-compile-remove(together-mode) */ + togetherModeStreamsMap?: TogetherModeStreamsState; + /* @conditional-compile-remove(together-mode) */ + togetherModeSeatingCoordinates?: TogetherModeParticipantSeatingState; }; /** @@ -71,7 +85,9 @@ export const videoGallerySelector: VideoGallerySelector = createSelector( isHideAttendeeNamesEnabled, getLocalParticipantReactionState, getSpotlightCallFeature, - getCapabilities + getCapabilities, + /* @conditional-compile-remove(together-mode) */ + getTogetherModeCallFeature ], ( screenShareRemoteParticipantId, @@ -88,7 +104,9 @@ export const videoGallerySelector: VideoGallerySelector = createSelector( isHideAttendeeNamesEnabled, localParticipantReaction, spotlightCallFeature, - capabilities + capabilities, + /* @conditional-compile-remove(together-mode) */ + togetherModeCallFeature ) => { const screenShareRemoteParticipant = screenShareRemoteParticipantId && remoteParticipants @@ -102,7 +120,14 @@ export const videoGallerySelector: VideoGallerySelector = createSelector( const noRemoteParticipants: RemoteParticipantState[] = []; const localParticipantReactionState = memoizedConvertToVideoTileReaction(localParticipantReaction); const spotlightedParticipantIds = memoizeSpotlightedParticipantIds(spotlightCallFeature?.spotlightedParticipants); - + /* @conditional-compile-remove(together-mode) */ + const togetherModeStreamsMap: TogetherModeStreamsProp = { + mainVideoStream: { + isAvailable: togetherModeCallFeature?.streams?.mainVideoStream?.isAvailable, + renderElement: togetherModeCallFeature?.streams?.mainVideoStream?.view?.target, + streamSize: togetherModeCallFeature?.streams?.mainVideoStream?.streamSize + } + }; return { screenShareParticipant: screenShareRemoteParticipant ? convertRemoteParticipantToVideoGalleryRemoteParticipant( @@ -139,7 +164,15 @@ export const videoGallerySelector: VideoGallerySelector = createSelector( dominantSpeakers: dominantSpeakerIds, maxRemoteVideoStreams: optimalVideoCount, spotlightedParticipants: spotlightedParticipantIds, - maxParticipantsToSpotlight: spotlightCallFeature?.maxParticipantsToSpotlight + maxParticipantsToSpotlight: spotlightCallFeature?.maxParticipantsToSpotlight, + /* @conditional-compile-remove(together-mode) */ + togetherModeStreams: togetherModeStreamsMap, + /* @conditional-compile-remove(together-mode) */ + togetherModeSeatingCoordinates: togetherModeCallFeature?.seatingPositions, + /* @conditional-compile-remove(together-mode) */ + isTogetherModeActive: togetherModeCallFeature?.isActive, + /* @conditional-compile-remove(together-mode) */ + canStartTogetherMode: capabilities?.startTogetherMode.isPresent }; } ); diff --git a/packages/calling-stateful-client/src/CallClientState.ts b/packages/calling-stateful-client/src/CallClientState.ts index a4126c0783b..72aa8bd98c3 100644 --- a/packages/calling-stateful-client/src/CallClientState.ts +++ b/packages/calling-stateful-client/src/CallClientState.ts @@ -302,6 +302,14 @@ export interface TogetherModeSeatingPositionState { height: number; } +/* @conditional-compile-remove(together-mode) */ +/** + * Represents the seating positions of participants in Together Mode. + * + * @beta + */ +export type TogetherModeParticipantSeatingState = Record; + /* @conditional-compile-remove(together-mode) */ /** * Interface representing the streams in Together Mode. @@ -327,7 +335,7 @@ export interface TogetherModeCallFeatureState { /** * Proxy of {@link @azure/communication-calling#TogetherModeCallFeature.TogetherModeSeatingMap}. */ - seatingPositions: Record; + seatingPositions: TogetherModeParticipantSeatingState; } /** diff --git a/packages/calling-stateful-client/src/index-public.ts b/packages/calling-stateful-client/src/index-public.ts index 2fef80e56af..88bdf6bff4e 100644 --- a/packages/calling-stateful-client/src/index-public.ts +++ b/packages/calling-stateful-client/src/index-public.ts @@ -37,6 +37,7 @@ export type { TogetherModeCallFeatureState as TogetherModeCallFeature } from './ export type { CallFeatureStreamState, TogetherModeSeatingPositionState, + TogetherModeParticipantSeatingState, CallFeatureStreamName, TogetherModeStreamsState } from './CallClientState'; diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index 02bd05f914b..ff6a10bd21e 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -906,6 +906,7 @@ export interface CallCompositeStrings { moreButtonGalleryPositionToggleLabel?: string; moreButtonGallerySpeakerLayoutLabel?: string; moreButtonLargeGalleryDefaultLayoutLabel?: string; + moreButtonTogetherModeLayoutLabel?: string; muteAllCancelButtonLabel: string; muteAllConfirmButtonLabel: string; muteAllDialogContent: string; @@ -4763,7 +4764,7 @@ export type SpotlightChangedListener = (args: { // @public export interface SpotlightPromptStrings { - closeSpotlightPromptButtonLabel: string; + closeSpotlightPromptButtonLabel?: string; startSpotlightCancelButtonLabel: string; startSpotlightConfirmButtonLabel: string; startSpotlightHeading: string; @@ -4994,10 +4995,28 @@ export const toFlatCommunicationIdentifier: (identifier: CommunicationIdentifier export interface TogetherModeCallFeature { // (undocumented) isActive: boolean; - seatingPositions: Record; + seatingPositions: TogetherModeParticipantSeatingState; streams: TogetherModeStreamsState; } +// @beta +export type TogetherModeParticipantSeatingProp = Record; + +// @beta +export type TogetherModeParticipantSeatingState = Record; + +// @beta +export interface TogetherModeSeatingPositionProp { + // (undocumented) + height: number; + // (undocumented) + left: number; + // (undocumented) + top: number; + // (undocumented) + width: number; +} + // @beta export interface TogetherModeSeatingPositionState { // (undocumented) @@ -5010,6 +5029,12 @@ export interface TogetherModeSeatingPositionState { width: number; } +// @beta +export interface TogetherModeStreamsProp { + // (undocumented) + mainVideoStream?: TogetherModeVideoStreamProp; +} + // @beta export interface TogetherModeStreamsState { // (undocumented) @@ -5022,6 +5047,17 @@ export interface TogetherModeStreamViewResult { mainVideoView?: CreateVideoStreamViewResult; } +// @beta +export interface TogetherModeVideoStreamProp { + // (undocumented) + isAvailable?: boolean; + renderElement?: HTMLElement; + streamSize?: { + width: number; + height: number; + }; +} + // @public export type TopicChangedListener = (event: { topic: string; @@ -5241,7 +5277,7 @@ export interface VideoBackgroundReplacementEffect extends BackgroundReplacementC export const VideoGallery: (props: VideoGalleryProps) => JSX.Element; // @public (undocumented) -export type VideoGalleryLayout = 'default' | 'floatingLocalVideo' | 'speaker' | /* @conditional-compile-remove(large-gallery) */ 'largeGallery' | 'focusedContent'; +export type VideoGalleryLayout = 'default' | 'floatingLocalVideo' | 'speaker' | /* @conditional-compile-remove(large-gallery) */ 'largeGallery' | /* @conditional-compile-remove(together-mode) */ 'togetherMode' | 'focusedContent'; // @public export interface VideoGalleryLocalParticipant extends VideoGalleryParticipant { @@ -5262,7 +5298,11 @@ export type VideoGalleryParticipant = { // @public export interface VideoGalleryProps { + // (undocumented) + canStartTogetherMode?: boolean; dominantSpeakers?: string[]; + // (undocumented) + isTogetherModeActive?: boolean; layout?: VideoGalleryLayout; localParticipant: VideoGalleryLocalParticipant; localVideoCameraCycleButtonProps?: LocalVideoCameraCycleButtonProps; @@ -5272,19 +5312,26 @@ export interface VideoGalleryProps { maxRemoteVideoStreams?: number; onCreateLocalStreamView?: (options?: VideoStreamOptions) => Promise; onCreateRemoteStreamView?: (userId: string, options?: VideoStreamOptions) => Promise; + // (undocumented) + onCreateTogetherModeStreamView?: (options?: VideoStreamOptions) => Promise; onDisposeLocalScreenShareStreamView?: () => Promise; onDisposeLocalStreamView?: () => void; onDisposeRemoteScreenShareStreamView?: (userId: string) => Promise; // @deprecated (undocumented) onDisposeRemoteStreamView?: (userId: string) => Promise; onDisposeRemoteVideoStreamView?: (userId: string) => Promise; + // (undocumented) + onDisposeTogetherModeStreamViews?: () => Promise; onMuteParticipant?: (userId: string) => Promise; onPinParticipant?: (userId: string) => void; onRenderAvatar?: OnRenderAvatarCallback; onRenderLocalVideoTile?: (localParticipant: VideoGalleryLocalParticipant) => JSX.Element; onRenderRemoteVideoTile?: (remoteParticipant: VideoGalleryRemoteParticipant) => JSX.Element; + // (undocumented) + onSetTogetherModeSceneSize?: (width: number, height: number) => void; onStartLocalSpotlight?: () => Promise; onStartRemoteSpotlight?: (userIds: string[]) => Promise; + onStartTogetherMode?: () => Promise; onStopLocalSpotlight?: () => Promise; onStopRemoteSpotlight?: (userIds: string[]) => Promise; onUnpinParticipant?: (userId: string) => void; @@ -5299,6 +5346,10 @@ export interface VideoGalleryProps { spotlightedParticipants?: string[]; strings?: Partial; styles?: VideoGalleryStyles; + // (undocumented) + togetherModeSeatingCoordinates?: TogetherModeParticipantSeatingProp; + // (undocumented) + togetherModeStreams?: TogetherModeStreamsProp; videoTilesOptions?: VideoTilesOptions; } @@ -5320,6 +5371,10 @@ export type VideoGallerySelector = (state: CallClientState, props: CallingBaseSe optimalVideoCount?: number; spotlightedParticipants?: string[]; maxParticipantsToSpotlight?: number; + isTogetherModeActive?: boolean; + canStartTogetherMode?: boolean; + togetherModeStreamsMap?: TogetherModeStreamsState; + togetherModeSeatingCoordinates?: TogetherModeParticipantSeatingState; }; // @public diff --git a/packages/communication-react/review/stable/communication-react.api.md b/packages/communication-react/review/stable/communication-react.api.md index 15d2ab2b050..587fa9c5547 100644 --- a/packages/communication-react/review/stable/communication-react.api.md +++ b/packages/communication-react/review/stable/communication-react.api.md @@ -710,6 +710,7 @@ export interface CallCompositeStrings { moreButtonGalleryPositionToggleLabel?: string; moreButtonGallerySpeakerLayoutLabel?: string; moreButtonLargeGalleryDefaultLayoutLabel?: string; + moreButtonTogetherModeLayoutLabel?: string; muteAllCancelButtonLabel: string; muteAllConfirmButtonLabel: string; muteAllDialogContent: string; @@ -4061,7 +4062,7 @@ export type SpotlightChangedListener = (args: { // @public export interface SpotlightPromptStrings { - closeSpotlightPromptButtonLabel: string; + closeSpotlightPromptButtonLabel?: string; startSpotlightCancelButtonLabel: string; startSpotlightConfirmButtonLabel: string; startSpotlightHeading: string; diff --git a/packages/communication-react/src/index.ts b/packages/communication-react/src/index.ts index 89c4477c335..0326f6912a9 100644 --- a/packages/communication-react/src/index.ts +++ b/packages/communication-react/src/index.ts @@ -312,7 +312,13 @@ export type { } from '../../react-components/src'; /* @conditional-compile-remove(together-mode) */ -export type { TogetherModeStreamViewResult } from '../../react-components/src'; +export type { + TogetherModeStreamViewResult, + TogetherModeStreamsProp, + TogetherModeParticipantSeatingProp, + TogetherModeVideoStreamProp, + TogetherModeSeatingPositionProp +} from '../../react-components/src'; export type { RaiseHandButtonProps, RaiseHandButtonStrings, RaisedHand } from '../../react-components/src'; export type { diff --git a/packages/react-components/src/components/MeetingReactionOverlay.tsx b/packages/react-components/src/components/MeetingReactionOverlay.tsx index 8566c089f38..9e647a31c1f 100644 --- a/packages/react-components/src/components/MeetingReactionOverlay.tsx +++ b/packages/react-components/src/components/MeetingReactionOverlay.tsx @@ -8,9 +8,13 @@ import { VideoGalleryLocalParticipant, VideoGalleryRemoteParticipant } from '../types'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeParticipantSeatingProp } from '../types'; import React, { useLayoutEffect, useRef, useState } from 'react'; import { ParticipantVideoTileOverlay } from './VideoGallery/ParticipantVideoTileOverlay'; import { RemoteContentShareReactionOverlay } from './VideoGallery/RemoteContentShareReactionOverlay'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeOverlay } from './TogetherModeOverlay'; /** * Reaction overlay component props @@ -40,6 +44,9 @@ export interface MeetingReactionOverlayProps { * Remote participant's reaction event. */ remoteParticipants?: VideoGalleryRemoteParticipant[]; + + /* @conditional-compile-remove(together-mode) */ + seatingCoordinates?: TogetherModeParticipantSeatingProp; } /** @@ -125,6 +132,19 @@ export const MeetingReactionOverlay = (props: MeetingReactionOverlayProps): JSX. /> ); + } else if (props.overlayMode === 'together-mode') { + /* @conditional-compile-remove(together-mode) */ + return ( +
+ +
+ ); + return <>; } else { return <>; } diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx new file mode 100644 index 00000000000..990fd53ad22 --- /dev/null +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -0,0 +1,277 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* @conditional-compile-remove(together-mode) */ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +/* @conditional-compile-remove(together-mode) */ +import { + Reaction, + ReactionResources, + TogetherModeParticipantSeatingProp, + VideoGalleryLocalParticipant, + VideoGalleryRemoteParticipant +} from '../types'; +/* @conditional-compile-remove(together-mode) */ +import { + getReactionStyleBucket, + IReactionStyleBucket, + ITogetherModeReactionStyleBucket, + opacityAnimationStyles, + spriteAnimationStyles +} from './styles/ReactionOverlay.style'; +/* @conditional-compile-remove(together-mode) */ +import { + getCombinedKey, + REACTION_NUMBER_OF_ANIMATION_FRAMES, + REACTION_START_DISPLAY_SIZE +} from './VideoGallery/utils/reactionUtils'; +/* @conditional-compile-remove(together-mode) */ +import { mergeStyles, Stack } from '@fluentui/react'; +/* @conditional-compile-remove(together-mode) */ +import { videoContainerStyles } from './styles/VideoTile.styles'; +/* @conditional-compile-remove(together-mode) */ +import { getEmojiResource } from './VideoGallery/utils/videoGalleryLayoutUtils'; +/* @conditional-compile-remove(together-mode) */ +import { useLocale } from '../localization'; +/* @conditional-compile-remove(together-mode) */ +import { _HighContrastAwareIcon } from './HighContrastAwareIcon'; + +/* @conditional-compile-remove(together-mode) */ +/** + * Reaction overlay component props + * + * Can be used with {@link VideoTile}. + * + * @internal + */ +type VisibleTogetherModeReaction = { + reaction: Reaction; + id: string; + styleBucket: ITogetherModeReactionStyleBucket; + displayName?: string; +}; + +// /** +// * Reaction overlay component props +// * +// * Can be used with {@link VideoTile}. +// * +// * @internal +// */ +// type VisibleSignaling = { +// isHandRaised?: boolean; +// isSpotlighted?: boolean; +// styleBucket: ITogetherModeReactionStyleBucket; +// displayName?: string; +// }; + +/* @conditional-compile-remove(together-mode) */ +type ReceivedReaction = { + id: string; + status: 'animating' | 'completedAnimating' | 'ignored'; +}; + +/* @conditional-compile-remove(together-mode) */ +/** + * TogetherModeOverlay component renders an empty JSX element. + * + * @returns {JSX.Element} An empty JSX element. + */ +export const TogetherModeOverlay = React.memo( + (props: { + reactionResources: ReactionResources; + localParticipant?: VideoGalleryLocalParticipant; + remoteParticipants?: VideoGalleryRemoteParticipant[]; + participantsSeatingArrangement?: TogetherModeParticipantSeatingProp; + }) => { + const locale = useLocale(); + const { reactionResources, remoteParticipants, localParticipant, participantsSeatingArrangement } = props; + // Reactions that are currently being animated + const [visibleReactions, setVisibleReactions] = useState([]); + // const [pinDisplayName, setPinDisplayName] = useState>({}); + + // Dictionary of userId to a reaction status. This is used to track the latest received reaction + // per user to avoid animating the same reaction multiple times and to limit the number of + // active reactions of a certain type. + const latestReceivedReaction = useRef>({}); + + const particpantsReactions: Reaction[] = useMemo(() => { + const reactions = + remoteParticipants + ?.map((remoteParticipant) => remoteParticipant.reaction) + .filter((reaction): reaction is Reaction => !!reaction) ?? []; + if (localParticipant?.reaction) { + reactions.push(localParticipant.reaction); + } + return reactions; + }, [remoteParticipants, localParticipant]); + + // const participantsRaiseHand: Record = useMemo(() => { + // const raiseHandParticipants: Record = {}; + // remoteParticipants?.forEach((participant) => { + // if (participant.raisedHand) { + // raiseHandParticipants[participant.userId] = true; + // } + // }); + // if (localParticipant?.raisedHand) { + // raiseHandParticipants[localParticipant.userId] = true; + // } + // return raiseHandParticipants; + // }, [localParticipant, remoteParticipants]); + + // const participantsSpotlight: Record = useMemo(() => { + // const spotlightParticipants: Record = {}; + // remoteParticipants?.forEach((participant) => { + // if (participant.spotlight) { + // spotlightParticipants[participant.userId] = true; + // } + // }); + // if (localParticipant?.spotlight) { + // spotlightParticipants[localParticipant.userId] = true; + // } + // return spotlightParticipants; + // }, [localParticipant, remoteParticipants]); + + const updateVisibleReactions = useCallback( + ( + reaction: Reaction, + userId: string, + displayName: string, + seatingPosition: { width: number; height: number; top: number; left: number } + ): void => { + const combinedKey = getCombinedKey(userId, reaction.reactionType, reaction.receivedOn); + + const alreadyHandled = latestReceivedReaction.current[userId]?.id === combinedKey; + if (alreadyHandled) { + return; + } + + const reactionStyle: ITogetherModeReactionStyleBucket = { + sizeScale: 0.9, + opacityMax: 0.9, + width: seatingPosition.width, + height: seatingPosition.height, + left: seatingPosition.left, + top: seatingPosition.top + }; + + latestReceivedReaction.current[userId] = { + id: combinedKey, + status: 'animating' + }; + + setVisibleReactions([ + ...visibleReactions, + { + reaction: reaction, + id: combinedKey, + displayName: displayName, + styleBucket: reactionStyle + } + ]); + return; + }, + [visibleReactions] + ); + + const removeVisibleReaction = (reactionType: string, id: string): void => { + setVisibleReactions(visibleReactions.filter((reaction) => reaction.id !== id)); + Object.entries(latestReceivedReaction.current).forEach(([userId, reaction]) => { + const userLastReaction = latestReceivedReaction.current[userId]; + if (reaction.id === id && userLastReaction) { + userLastReaction.status = 'completedAnimating'; + } + }); + }; + + // Update visible reactions when remote participants send a reaction + useEffect(() => { + remoteParticipants?.map((participant) => { + if (participant?.reaction && participant.videoStream?.isAvailable) { + const seatingPosition = participantsSeatingArrangement?.[participant.userId]; + const displayName = !participant?.displayName + ? locale.strings.videoGallery.displayNamePlaceholder + : participant?.displayName; + seatingPosition && + updateVisibleReactions(participant.reaction, participant.userId, displayName, seatingPosition); + } + }); + if (localParticipant?.reaction && localParticipant.videoStream?.isAvailable) { + const seatingPosition = participantsSeatingArrangement?.[localParticipant.userId]; + const displayName = !localParticipant?.displayName + ? locale.strings.videoGallery.displayNamePlaceholder + : localParticipant?.displayName; + seatingPosition && + updateVisibleReactions(localParticipant.reaction, localParticipant.userId, displayName, seatingPosition); + } + }, [ + locale.strings.videoGallery.displayNamePlaceholder, + particpantsReactions, + remoteParticipants, + localParticipant, + updateVisibleReactions, + participantsSeatingArrangement + ]); + + const styleBucket = (): IReactionStyleBucket => getReactionStyleBucket(); + const displaySizePx = (): number => REACTION_START_DISPLAY_SIZE * styleBucket().sizeScale; + return ( + + {visibleReactions.map((reaction) => ( +
+
+ { + // First div - Section that fixes the travel height and applies the movement animation + // Second div - Keeps track of active sprites and responsible for marking, counting + // and removing reactions. Responsible for opacity controls as the sprite emoji animates + // Third div - Responsible for calculating the point of X axis where the reaction will start animation + // Fourth div - Play Animation as the other animation applies on the base play animation for the sprite +
+
{ + removeVisibleReaction(reaction.reaction.reactionType, reaction.id); + }} + style={opacityAnimationStyles(reaction.styleBucket.opacityMax)} + > +
+
+
+
+
+ } + {/*
+
+ <_HighContrastAwareIcon disabled={false} iconName="ControlButtonRaiseHand" /> +
+
{reaction.displayName}
+
+ <_HighContrastAwareIcon iconName={'Muted'} /> + <_HighContrastAwareIcon iconName={'ControlButtonExitSpotlight'} /> +
+
*/} +
+
+ ))} + + ); + } +); diff --git a/packages/react-components/src/components/VideoGallery.tsx b/packages/react-components/src/components/VideoGallery.tsx index 7311d188744..7c384be0bc1 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -16,6 +16,12 @@ import { VideoStreamOptions, CreateVideoStreamViewResult } from '../types'; +/* @conditional-compile-remove(together-mode) */ +import { + TogetherModeParticipantSeatingProp, + TogetherModeStreamsProp, + TogetherModeStreamViewResult +} from '../types/TogetherModeTypes'; import { ViewScalingMode } from '../types'; import { HorizontalGalleryStyles } from './HorizontalGallery'; import { _RemoteVideoTile } from './RemoteVideoTile'; @@ -37,8 +43,15 @@ import { SpeakerVideoLayout } from './VideoGallery/SpeakerVideoLayout'; import { FocusedContentLayout } from './VideoGallery/FocusContentLayout'; /* @conditional-compile-remove(large-gallery) */ import { LargeGalleryLayout } from './VideoGallery/LargeGalleryLayout'; + +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeLayout } from './VideoGallery/TogetherModeLayout'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeLayoutProps } from './VideoGallery/Layout'; import { LayoutProps } from './VideoGallery/Layout'; import { ReactionResources } from '../types/ReactionTypes'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeStream } from './VideoGallery/TogetherModeStream'; /** * @private @@ -142,6 +155,7 @@ export type VideoGalleryLayout = | 'floatingLocalVideo' | 'speaker' | /* @conditional-compile-remove(large-gallery) */ 'largeGallery' + | /* @conditional-compile-remove(together-mode) */ 'togetherMode' | 'focusedContent'; /** @@ -313,6 +327,23 @@ export interface VideoGalleryProps { * This callback is to mute a remote participant */ onMuteParticipant?: (userId: string) => Promise; + /* @conditional-compile-remove(together-mode) */ + canStartTogetherMode?: boolean; + /* @conditional-compile-remove(together-mode) */ + isTogetherModeActive?: boolean; + /* @conditional-compile-remove(together-mode) */ + onCreateTogetherModeStreamView?: (options?: VideoStreamOptions) => Promise; + /* @conditional-compile-remove(together-mode) */ + /** Callback to create the local video stream view */ + onStartTogetherMode?: () => Promise; + /* @conditional-compile-remove(together-mode) */ + onSetTogetherModeSceneSize?: (width: number, height: number) => void; + /* @conditional-compile-remove(together-mode) */ + togetherModeStreams?: TogetherModeStreamsProp; + /* @conditional-compile-remove(together-mode) */ + togetherModeSeatingCoordinates?: TogetherModeParticipantSeatingProp; + /* @conditional-compile-remove(together-mode) */ + onDisposeTogetherModeStreamViews?: () => Promise; } /** @@ -397,7 +428,23 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { maxParticipantsToSpotlight, reactionResources, videoTilesOptions, - onMuteParticipant + onMuteParticipant, + /* @conditional-compile-remove(together-mode) */ + canStartTogetherMode, + /* @conditional-compile-remove(together-mode) */ + isTogetherModeActive, + /* @conditional-compile-remove(together-mode) */ + onCreateTogetherModeStreamView, + /* @conditional-compile-remove(together-mode) */ + onStartTogetherMode, + /* @conditional-compile-remove(together-mode) */ + onSetTogetherModeSceneSize, + /* @conditional-compile-remove(together-mode) */ + togetherModeStreams, + /* @conditional-compile-remove(together-mode) */ + togetherModeSeatingCoordinates, + /* @conditional-compile-remove(together-mode) */ + onDisposeTogetherModeStreamViews } = props; const ids = useIdentifiers(); @@ -746,6 +793,49 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { ] ); + /* @conditional-compile-remove(together-mode) */ + const togetherModeStreamComponent = useMemo( + () => ( + + ), + [ + canStartTogetherMode, + isTogetherModeActive, + onCreateTogetherModeStreamView, + onStartTogetherMode, + onSetTogetherModeSceneSize, + togetherModeStreams, + togetherModeSeatingCoordinates, + onDisposeTogetherModeStreamViews, + localParticipant, + remoteParticipants, + reactionResources, + containerWidth, + containerHeight + ] + ); + + /* @conditional-compile-remove(together-mode) */ + const togetherModeLayoutProps = useMemo(() => { + return { + togetherModeStreamComponent + }; + }, [togetherModeStreamComponent]); + const videoGalleryLayout = useMemo(() => { if (screenShareParticipant && layout === 'focusedContent') { return ; @@ -761,8 +851,17 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { if (layout === 'largeGallery') { return ; } + /* @conditional-compile-remove(together-mode) */ + if (layout === 'togetherMode') { + return ; + } return ; - }, [layout, layoutProps, screenShareParticipant]); + }, [ + layout, + layoutProps, + /* @conditional-compile-remove(together-mode) */ togetherModeLayoutProps, + screenShareParticipant + ]); return (
{ + return ( + <> + {props.togetherModeStreamComponent} + {/*

Chuk in together mode layout

*/} + + ); +}; diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx new file mode 100644 index 00000000000..e8dabe31b1e --- /dev/null +++ b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* @conditional-compile-remove(together-mode) */ +import React, { useEffect } from 'react'; +/* @conditional-compile-remove(together-mode) */ +import { _formatString, _pxToRem } from '@internal/acs-ui-common'; +/* @conditional-compile-remove(together-mode) */ +import { + ReactionResources, + TogetherModeParticipantSeatingProp, + TogetherModeStreamsProp, + TogetherModeStreamViewResult, + VideoGalleryLocalParticipant, + VideoGalleryRemoteParticipant, + VideoStreamOptions +} from '../../types'; +/* @conditional-compile-remove(together-mode) */ +import { StreamMedia } from '../StreamMedia'; +/* @conditional-compile-remove(together-mode) */ +import { MeetingReactionOverlay } from '../MeetingReactionOverlay'; +/* @conditional-compile-remove(together-mode) */ +import { Stack } from '@fluentui/react'; + +/* @conditional-compile-remove(together-mode) */ +/** + * A memoized version of local screen share component. React.memo is used for a performance + * boost by memoizing the same rendered component to avoid rerendering this when the parent component rerenders. + * https://reactjs.org/docs/react-api.html#reactmemo + */ +export const TogetherModeStream = React.memo( + (props: { + canStartTogetherMode?: boolean; + isTogetherModeActive?: boolean; + onCreateTogetherModeStreamView?: (options?: VideoStreamOptions) => Promise; + onStartTogetherMode?: (options?: VideoStreamOptions) => Promise; + onDisposeTogetherModeStreamViews?: () => Promise; + onSetTogetherModeSceneSize?: (width: number, height: number) => void; + togetherModeStreams?: TogetherModeStreamsProp; + seatingCoordinates?: TogetherModeParticipantSeatingProp; + reactionResources?: ReactionResources; + localParticipant?: VideoGalleryLocalParticipant; + remoteParticipants?: VideoGalleryRemoteParticipant[]; + containerWidth?: number; + containerHeight?: number; + }): React.ReactNode => { + const { + canStartTogetherMode, + isTogetherModeActive, + onCreateTogetherModeStreamView, + onStartTogetherMode, + onDisposeTogetherModeStreamViews, + onSetTogetherModeSceneSize, + togetherModeStreams, + seatingCoordinates, + reactionResources, + localParticipant, + remoteParticipants, + containerWidth, + containerHeight + } = props; + + if (canStartTogetherMode && !isTogetherModeActive) { + onStartTogetherMode && onStartTogetherMode(); + } + + if (!togetherModeStreams?.mainVideoStream?.renderElement) { + onCreateTogetherModeStreamView && onCreateTogetherModeStreamView(); + } + + useEffect(() => { + onSetTogetherModeSceneSize && + containerWidth && + containerHeight && + onSetTogetherModeSceneSize(containerWidth, containerHeight); + }, [onSetTogetherModeSceneSize, containerWidth, containerHeight]); + + useEffect(() => { + return () => { + !togetherModeStreams?.mainVideoStream?.isAvailable && + onDisposeTogetherModeStreamViews && + onDisposeTogetherModeStreamViews(); + }; + }, [onDisposeTogetherModeStreamViews, togetherModeStreams]); + return ( + <> + {containerWidth && containerHeight && ( + +
+ + {reactionResources && ( + + )} +
+
+ )} + + ); + } +); diff --git a/packages/react-components/src/components/styles/ReactionOverlay.style.ts b/packages/react-components/src/components/styles/ReactionOverlay.style.ts index 82c712e529d..36853f96b9b 100644 --- a/packages/react-components/src/components/styles/ReactionOverlay.style.ts +++ b/packages/react-components/src/components/styles/ReactionOverlay.style.ts @@ -136,6 +136,19 @@ export interface IReactionStyleBucket { heightMinScale?: number; } +/** + * @private + * Interface for defining the style bucket for reactions in Together Mode. + */ +export interface ITogetherModeReactionStyleBucket { + sizeScale: number; + opacityMax: number; + height: number; + width?: number; + left?: number; + top?: number; +} + /** * Return a style bucket based on the number of active sprites. * For example, the first three reactions should appear at maximum diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index 4113d086a37..9cad631ebbf 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -62,6 +62,15 @@ export type { ViewScalingMode } from './types'; +/* @conditional-compile-remove(together-mode) */ +export type { + TogetherModeStreamViewResult, + TogetherModeVideoStreamProp, + TogetherModeStreamsProp, + TogetherModeParticipantSeatingProp, + TogetherModeSeatingPositionProp +} from './types'; + export type { RaisedHand } from './types'; export type { Spotlight } from './types'; @@ -84,6 +93,3 @@ export type { SurveyIssues } from './types'; export type { SurveyIssuesHeadingStrings } from './types'; export type { CallSurveyImprovementSuggestions } from './types'; - -/* @conditional-compile-remove(together-mode) */ -export type { TogetherModeStreamViewResult } from './types'; diff --git a/packages/react-components/src/types/ReactionTypes.ts b/packages/react-components/src/types/ReactionTypes.ts index 4f28340ed98..2cd2a4cf1ae 100644 --- a/packages/react-components/src/types/ReactionTypes.ts +++ b/packages/react-components/src/types/ReactionTypes.ts @@ -47,4 +47,4 @@ export interface ReactionResources { * Options for overlay mode for reaction rendering * @internal */ -export type OverlayModeTypes = 'grid-tiles' | 'screen-share' | 'content-share'; +export type OverlayModeTypes = 'grid-tiles' | 'screen-share' | 'content-share' | 'together-mode'; diff --git a/packages/react-components/src/types/TogetherModeTypes.ts b/packages/react-components/src/types/TogetherModeTypes.ts index 4307a3777a3..a2eade35c00 100644 --- a/packages/react-components/src/types/TogetherModeTypes.ts +++ b/packages/react-components/src/types/TogetherModeTypes.ts @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - /* @conditional-compile-remove(together-mode) */ import { CreateVideoStreamViewResult } from './VideoGalleryParticipant'; @@ -18,7 +17,7 @@ export interface TogetherModeStreamViewResult { * Represents a video stream in Together Mode. * @beta */ -export interface TogetherModeVideoStream { +export interface TogetherModeVideoStreamProp { isAvailable?: boolean; /** * The HTML element used to render the video stream. @@ -38,24 +37,26 @@ export interface TogetherModeVideoStream { * Interface representing the streams in Together Mode. * @beta */ -export interface TogetherModeStreams { - mainVideoStream?: TogetherModeVideoStream; +export interface TogetherModeStreamsProp { + mainVideoStream?: TogetherModeVideoStreamProp; } +/* @conditional-compile-remove(together-mode) */ /** - * Interface representing the seating information in Together Mode. + * Represents the seating positions of participants in Together Mode. + * * @beta */ -export interface TogetherModeSeatingInfo { - top: number; - left: number; - width: number; - height: number; -} +export type TogetherModeParticipantSeatingProp = Record; /* @conditional-compile-remove(together-mode) */ /** * Interface representing the position of a participant in Together Mode. * @beta */ -export type TogetherModeParticipantPosition = Record; +export interface TogetherModeSeatingPositionProp { + top: number; + left: number; + width: number; + height: number; +} diff --git a/packages/react-composites/src/composites/CallComposite/Strings.tsx b/packages/react-composites/src/composites/CallComposite/Strings.tsx index f96d833ea4f..17e8faa2222 100644 --- a/packages/react-composites/src/composites/CallComposite/Strings.tsx +++ b/packages/react-composites/src/composites/CallComposite/Strings.tsx @@ -624,6 +624,11 @@ export interface CallCompositeStrings { * Label for the selection of the default (Gallery) layout */ moreButtonLargeGalleryDefaultLayoutLabel?: string; + /* conditional-compile-remove(together-mode) */ + /** + * Label for the selection of the default (Gallery) layout + */ + moreButtonTogetherModeLayoutLabel?: string; /** * Label for the selection of the floatingLocalVideo (Dynamic) layout */ diff --git a/packages/react-composites/src/composites/CallComposite/components/MediaGallery.tsx b/packages/react-composites/src/composites/CallComposite/components/MediaGallery.tsx index f9116e7289c..fde54f9dc85 100644 --- a/packages/react-composites/src/composites/CallComposite/components/MediaGallery.tsx +++ b/packages/react-composites/src/composites/CallComposite/components/MediaGallery.tsx @@ -188,7 +188,12 @@ export const MediaGallery = (props: MediaGalleryProps): JSX.Element => { const VideoGalleryMemoized = useMemo(() => { const layoutBasedOnUserSelection = (): VideoGalleryLayout => { - return props.localVideoTileOptions ? layoutBasedOnTilePosition : props.userSetGalleryLayout; + /* @conditional-compile-remove(together-mode) */ + if (videoGalleryProps?.isTogetherModeActive) { + return 'togetherMode'; + } else { + return props.localVideoTileOptions ? layoutBasedOnTilePosition : props.userSetGalleryLayout; + } return layoutBasedOnTilePosition; }; diff --git a/packages/react-composites/src/composites/CallComposite/selectors/baseSelectors.ts b/packages/react-composites/src/composites/CallComposite/selectors/baseSelectors.ts index 34cf4cb5c83..0690061a3df 100644 --- a/packages/react-composites/src/composites/CallComposite/selectors/baseSelectors.ts +++ b/packages/react-composites/src/composites/CallComposite/selectors/baseSelectors.ts @@ -307,3 +307,13 @@ export const getIsRoomsCall = (state: CallAdapterState): boolean => state.isRoom /** @private */ export const getVideoBackgroundImages = (state: CallAdapterState): VideoBackgroundImage[] | undefined => state.videoBackgroundImages; + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + * Gets the together mode streams state. + * @param state - The current state of the call adapter. + * @returns The together mode streams state or undefined. + */ +export const getIsTogetherModeActive = (state: CallAdapterState): boolean | undefined => + state.call?.togetherMode.isActive; diff --git a/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx b/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx index dcbfb8eddac..fb81d750298 100644 --- a/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx +++ b/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx @@ -26,6 +26,8 @@ import { _preventDismissOnEvent } from '@internal/acs-ui-common'; import { showDtmfDialer } from '../../CallComposite/utils/MediaGalleryUtils'; import { useSelector } from '../../CallComposite/hooks/useSelector'; import { getTargetCallees } from '../../CallComposite/selectors/baseSelectors'; +/* @conditional-compile-remove(together-mode) */ +import { getIsTogetherModeActive, getLatestCapabilitiesChangedInfo } from '../../CallComposite/selectors/baseSelectors'; import { getTeamsMeetingCoordinates, getIsTeamsMeeting } from '../../CallComposite/selectors/baseSelectors'; import { CallControlOptions } from '../../CallComposite'; @@ -72,6 +74,10 @@ export const DesktopMoreButton = (props: DesktopMoreButtonProps): JSX.Element => const isTeamsMeeting = useSelector(getIsTeamsMeeting); const teamsMeetingCoordinates = useSelector(getTeamsMeetingCoordinates); + /* @conditional-compile-remove(together-mode) */ + const isTogetherModeActive = useSelector(getIsTogetherModeActive); + /* @conditional-compile-remove(together-mode) */ + const latestCapabilities = useSelector(getLatestCapabilitiesChangedInfo); const [dtmfDialerChecked, setDtmfDialerChecked] = useState(props.dtmfDialerPresent ?? false); @@ -340,6 +346,25 @@ export const DesktopMoreButton = (props: DesktopMoreButtonProps): JSX.Element => } }; + /* @conditional-compile-remove(together-mode) */ + const togetherModeOption = { + key: 'togetherModeSelectionKey', + text: localeStrings.strings.call.moreButtonTogetherModeLayoutLabel, + canCheck: true, + itemProps: { + styles: buttonFlyoutIncreasedSizeStyles + }, + isChecked: props.userSetGalleryLayout === 'togetherMode', + onClick: () => { + props.onUserSetGalleryLayout && props.onUserSetGalleryLayout('togetherMode'); + setFocusedContentOn(false); + }, + iconProps: { + iconName: 'LargeGalleryLayout', + styles: { root: { lineHeight: 0 } } + } + }; + /* @conditional-compile-remove(overflow-top-composite) */ const overflowGalleryOption = { key: 'topKey', @@ -370,6 +395,10 @@ export const DesktopMoreButton = (props: DesktopMoreButtonProps): JSX.Element => galleryOptions.subMenuProps?.items?.push(galleryOption); /* @conditional-compile-remove(overflow-top-composite) */ galleryOptions.subMenuProps?.items?.push(overflowGalleryOption); + /* @conditional-compile-remove(together-mode) */ + if (latestCapabilities?.newValue.startTogetherMode?.isPresent || isTogetherModeActive) { + galleryOptions.subMenuProps?.items?.push(togetherModeOption); + } if (props.callControls === true || (props.callControls as CallControlOptions)?.galleryControlsButton !== false) { moreButtonContextualMenuItems.push(galleryOptions); } diff --git a/packages/react-composites/src/composites/localization/locales/en-US/strings.json b/packages/react-composites/src/composites/localization/locales/en-US/strings.json index 66d25f3e206..1f0824b16d3 100644 --- a/packages/react-composites/src/composites/localization/locales/en-US/strings.json +++ b/packages/react-composites/src/composites/localization/locales/en-US/strings.json @@ -235,6 +235,7 @@ "moreButtonGalleryDefaultLayoutLabel": "Gallery view", "moreButtonGalleryFocusedContentLayoutLabel": "Focus on content", "moreButtonLargeGalleryDefaultLayoutLabel": "Large Gallery", + "moreButtonGalleryTogetherModeLayoutLabel": "Together mode", "capabilityChangedNotification": { "turnVideoOn": { "lostDueToMeetingOption": "Your camera has been disabled. You can no longer share video.", @@ -327,8 +328,7 @@ "stopAllSpotlightText": "The videos will no longer be highlighted for everyone in the meeting.", "stopSpotlightConfirmButtonLabel": "Stop spotlighting", "stopSpotlightOnSelfConfirmButtonLabel": "Exit spotlight", - "stopSpotlightCancelButtonLabel": "Cancel", - "closeSpotlightPromptButtonLabel": "Close" + "stopSpotlightCancelButtonLabel": "Cancel" }, "exitSpotlightButtonLabel": "Exit spotlight", "exitSpotlightButtonTooltip": "Exit spotlight", From 8f2a758a6768eacf0329f39176c7e87d75840080 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Mon, 11 Nov 2024 10:08:46 +0000 Subject: [PATCH 08/37] Base changes for together Mode stream and reactions --- .../src/handlers/createCommonHandlers.ts | 4 +- .../src/videoGallerySelector.ts | 4 +- .../src/StatefulCallClient.ts | 37 ++++++++----------- .../review/beta/communication-react.api.md | 37 ++++++++++++++++++- packages/communication-react/src/index.ts | 8 ++-- .../src/components/MeetingReactionOverlay.tsx | 4 +- .../src/components/TogetherModeOverlay.tsx | 4 +- .../src/components/VideoGallery.tsx | 8 ++-- .../VideoGallery/TogetherModeLayout.tsx | 7 +--- .../VideoGallery/TogetherModeStream.tsx | 8 ++-- packages/react-components/src/index.ts | 11 ++---- .../CallComposite/components/Prompt.tsx | 2 +- .../localization/locales/en-US/strings.json | 2 +- 13 files changed, 78 insertions(+), 58 deletions(-) diff --git a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts index 9bc254b8192..9cdce5d4337 100644 --- a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts +++ b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts @@ -774,13 +774,13 @@ export const createDefaultCommonCallingHandlers = memoizeOne( const mainVideoStream = togetherModeStreams.mainVideoStream; if (mainVideoStream && mainVideoStream.isAvailable && !mainVideoStream.view) { - const createViewResult = await callClient.createView(call.id, mainVideoStream, options); + // const createViewResult = await callClient.createView(call.id, mainVideoStream, options); + const createViewResult = await callClient.createView(call.id, undefined, mainVideoStream, options); // SDK currently only supports 1 Video media stream type togetherModeCreateViewResult.mainVideoView = createViewResult?.view ? { view: createViewResult?.view } : undefined; } - return togetherModeCreateViewResult; }; diff --git a/packages/calling-component-bindings/src/videoGallerySelector.ts b/packages/calling-component-bindings/src/videoGallerySelector.ts index ebbd56f1154..0bcc312f9e8 100644 --- a/packages/calling-component-bindings/src/videoGallerySelector.ts +++ b/packages/calling-component-bindings/src/videoGallerySelector.ts @@ -7,7 +7,7 @@ import { CallClientState, RemoteParticipantState } from '@internal/calling-state import { TogetherModeParticipantSeatingState, TogetherModeStreamsState } from '@internal/calling-stateful-client'; import { VideoGalleryRemoteParticipant, VideoGalleryLocalParticipant } from '@internal/react-components'; /* @conditional-compile-remove(together-mode) */ -import { TogetherModeStreamsProp } from '@internal/react-components'; +import { VideoGalleryTogetherModeStreams } from '@internal/react-components'; import { createSelector } from 'reselect'; import { CallingBaseSelectorProps, @@ -121,7 +121,7 @@ export const videoGallerySelector: VideoGallerySelector = createSelector( const localParticipantReactionState = memoizedConvertToVideoTileReaction(localParticipantReaction); const spotlightedParticipantIds = memoizeSpotlightedParticipantIds(spotlightCallFeature?.spotlightedParticipants); /* @conditional-compile-remove(together-mode) */ - const togetherModeStreamsMap: TogetherModeStreamsProp = { + const togetherModeStreamsMap: VideoGalleryTogetherModeStreams = { mainVideoStream: { isAvailable: togetherModeCallFeature?.streams?.mainVideoStream?.isAvailable, renderElement: togetherModeCallFeature?.streams?.mainVideoStream?.view?.target, diff --git a/packages/calling-stateful-client/src/StatefulCallClient.ts b/packages/calling-stateful-client/src/StatefulCallClient.ts index 60721d8f98a..991ca5e08ea 100644 --- a/packages/calling-stateful-client/src/StatefulCallClient.ts +++ b/packages/calling-stateful-client/src/StatefulCallClient.ts @@ -423,9 +423,16 @@ export const createStatefulCallClientWithDeps = ( value: async ( callId: string | undefined, participantId: CommunicationIdentifier | undefined, - stream: LocalVideoStreamState | RemoteVideoStreamState, + stream: + | LocalVideoStreamState + | RemoteVideoStreamState + | /* @conditional-compile-remove(together-mode) */ CallFeatureStreamState, options?: CreateViewOptions ): Promise => { + /* @conditional-compile-remove(together-mode) */ + if ('feature' in stream) { + return await createCallFeatureView(context, internalContext, callId, stream, options); + } const participantIdKind = participantId ? getIdentifierKind(participantId) : undefined; const result = await createView(context, internalContext, callId, participantIdKind, stream, options); // We only need to declaratify the VideoStreamRendererView object for remote participants. Because the updateScalingMode only needs to be called on remote participant stream views. @@ -436,36 +443,24 @@ export const createStatefulCallClientWithDeps = ( return result; } }); - /* @conditional-compile-remove(together-mode) */ - Object.defineProperty(callClient, 'createCallFeatureView', { - configurable: false, - value: async ( - callId: string | undefined, - stream: CallFeatureStreamState, - options?: CreateViewOptions - ): Promise => { - const result = await createCallFeatureView(context, internalContext, callId, stream, options); - return result; - } - }); Object.defineProperty(callClient, 'disposeView', { configurable: false, value: ( callId: string | undefined, participantId: CommunicationIdentifier | undefined, - stream: LocalVideoStreamState | RemoteVideoStreamState + stream: + | LocalVideoStreamState + | RemoteVideoStreamState + | /* @conditional-compile-remove(together-mode) */ CallFeatureStreamState ): void => { + /* @conditional-compile-remove(together-mode) */ + if ('feature' in stream) { + disposeCallFeatureView(context, internalContext, callId, stream); + } const participantIdKind = participantId ? getIdentifierKind(participantId) : undefined; disposeView(context, internalContext, callId, participantIdKind, stream); } }); - /* @conditional-compile-remove(together-mode) */ - Object.defineProperty(callClient, 'disposeCallFeatureView', { - configurable: false, - value: (callId: string | undefined, stream: CallFeatureStreamState): void => { - disposeCallFeatureView(context, internalContext, callId, stream); - } - }); const newStatefulCallClient = new Proxy( callClient, diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index bf5b128a224..878f7591386 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -5050,6 +5050,18 @@ export interface TogetherModeSeatingPositionState { width: number; } +// @beta +export interface TogetherModeStream { + isAvailable?: boolean; + isReceiving?: boolean; + renderElement?: HTMLElement; + scalingMode?: ViewScalingMode; + streamSize?: { + width: number; + height: number; + }; +} + // @beta export interface TogetherModeStreamsState { // (undocumented) @@ -5353,9 +5365,9 @@ export interface VideoGalleryProps { strings?: Partial; styles?: VideoGalleryStyles; // (undocumented) - togetherModeSeatingCoordinates?: TogetherModeParticipantSeatingProp; + togetherModeSeatingCoordinates?: VideoGalleryTogetherModeParticipantPosition; // (undocumented) - togetherModeStreams?: TogetherModeStreamsProp; + togetherModeStreams?: VideoGalleryTogetherModeStreams; videoTilesOptions?: VideoTilesOptions; } @@ -5436,6 +5448,27 @@ export interface VideoGalleryStyles extends BaseCustomStyles { verticalGallery?: VerticalGalleryStyles; } +// @beta +export type VideoGalleryTogetherModeParticipantPosition = Record; + +// @beta +export interface VideoGalleryTogetherModeSeatingInfo { + // (undocumented) + height: number; + // (undocumented) + left: number; + // (undocumented) + top: number; + // (undocumented) + width: number; +} + +// @beta +export interface VideoGalleryTogetherModeStreams { + // (undocumented) + mainVideoStream?: TogetherModeStream; +} + // @public export interface VideoStreamOptions { isMirrored?: boolean; diff --git a/packages/communication-react/src/index.ts b/packages/communication-react/src/index.ts index ca3a993c259..88ab7c073f2 100644 --- a/packages/communication-react/src/index.ts +++ b/packages/communication-react/src/index.ts @@ -314,10 +314,10 @@ export type { /* @conditional-compile-remove(together-mode) */ export type { TogetherModeStreamViewResult, - TogetherModeStreamsProp, - TogetherModeParticipantSeatingProp, - TogetherModeVideoStreamProp, - TogetherModeSeatingPositionProp + VideoGalleryTogetherModeStreams, + VideoGalleryTogetherModeParticipantPosition, + VideoGalleryTogetherModeSeatingInfo, + TogetherModeStream } from '../../react-components/src'; export type { RaiseHandButtonProps, RaiseHandButtonStrings, RaisedHand } from '../../react-components/src'; diff --git a/packages/react-components/src/components/MeetingReactionOverlay.tsx b/packages/react-components/src/components/MeetingReactionOverlay.tsx index 9e647a31c1f..e59a7d6d7d9 100644 --- a/packages/react-components/src/components/MeetingReactionOverlay.tsx +++ b/packages/react-components/src/components/MeetingReactionOverlay.tsx @@ -9,7 +9,7 @@ import { VideoGalleryRemoteParticipant } from '../types'; /* @conditional-compile-remove(together-mode) */ -import { TogetherModeParticipantSeatingProp } from '../types'; +import { VideoGalleryTogetherModeParticipantPosition } from '../types'; import React, { useLayoutEffect, useRef, useState } from 'react'; import { ParticipantVideoTileOverlay } from './VideoGallery/ParticipantVideoTileOverlay'; import { RemoteContentShareReactionOverlay } from './VideoGallery/RemoteContentShareReactionOverlay'; @@ -46,7 +46,7 @@ export interface MeetingReactionOverlayProps { remoteParticipants?: VideoGalleryRemoteParticipant[]; /* @conditional-compile-remove(together-mode) */ - seatingCoordinates?: TogetherModeParticipantSeatingProp; + seatingCoordinates?: VideoGalleryTogetherModeParticipantPosition; } /** diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index 990fd53ad22..2b880c1a971 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Reaction, ReactionResources, - TogetherModeParticipantSeatingProp, + VideoGalleryTogetherModeParticipantPosition, VideoGalleryLocalParticipant, VideoGalleryRemoteParticipant } from '../types'; @@ -82,7 +82,7 @@ export const TogetherModeOverlay = React.memo( reactionResources: ReactionResources; localParticipant?: VideoGalleryLocalParticipant; remoteParticipants?: VideoGalleryRemoteParticipant[]; - participantsSeatingArrangement?: TogetherModeParticipantSeatingProp; + participantsSeatingArrangement?: VideoGalleryTogetherModeParticipantPosition; }) => { const locale = useLocale(); const { reactionResources, remoteParticipants, localParticipant, participantsSeatingArrangement } = props; diff --git a/packages/react-components/src/components/VideoGallery.tsx b/packages/react-components/src/components/VideoGallery.tsx index ca956e2534c..c35658f8de2 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -18,8 +18,8 @@ import { } from '../types'; /* @conditional-compile-remove(together-mode) */ import { - TogetherModeParticipantSeatingProp, - TogetherModeStreamsProp, + VideoGalleryTogetherModeParticipantPosition, + VideoGalleryTogetherModeStreams, TogetherModeStreamViewResult } from '../types/TogetherModeTypes'; import { ViewScalingMode } from '../types'; @@ -339,9 +339,9 @@ export interface VideoGalleryProps { /* @conditional-compile-remove(together-mode) */ onSetTogetherModeSceneSize?: (width: number, height: number) => void; /* @conditional-compile-remove(together-mode) */ - togetherModeStreams?: TogetherModeStreamsProp; + togetherModeStreams?: VideoGalleryTogetherModeStreams; /* @conditional-compile-remove(together-mode) */ - togetherModeSeatingCoordinates?: TogetherModeParticipantSeatingProp; + togetherModeSeatingCoordinates?: VideoGalleryTogetherModeParticipantPosition; /* @conditional-compile-remove(together-mode) */ onDisposeTogetherModeStreamViews?: () => Promise; } diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx index bf013e10103..cc1f2ef71b4 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx @@ -11,10 +11,5 @@ import { TogetherModeLayoutProps } from './Layout'; * https://reactjs.org/docs/react-api.html#reactmemo */ export const TogetherModeLayout = (props: TogetherModeLayoutProps): JSX.Element => { - return ( - <> - {props.togetherModeStreamComponent} - {/*

Chuk in together mode layout

*/} - - ); + return <>{props.togetherModeStreamComponent}; }; diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx index e8dabe31b1e..596576a5f9b 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx @@ -8,8 +8,8 @@ import { _formatString, _pxToRem } from '@internal/acs-ui-common'; /* @conditional-compile-remove(together-mode) */ import { ReactionResources, - TogetherModeParticipantSeatingProp, - TogetherModeStreamsProp, + VideoGalleryTogetherModeParticipantPosition, + VideoGalleryTogetherModeStreams, TogetherModeStreamViewResult, VideoGalleryLocalParticipant, VideoGalleryRemoteParticipant, @@ -36,8 +36,8 @@ export const TogetherModeStream = React.memo( onStartTogetherMode?: (options?: VideoStreamOptions) => Promise; onDisposeTogetherModeStreamViews?: () => Promise; onSetTogetherModeSceneSize?: (width: number, height: number) => void; - togetherModeStreams?: TogetherModeStreamsProp; - seatingCoordinates?: TogetherModeParticipantSeatingProp; + togetherModeStreams?: VideoGalleryTogetherModeStreams; + seatingCoordinates?: VideoGalleryTogetherModeParticipantPosition; reactionResources?: ReactionResources; localParticipant?: VideoGalleryLocalParticipant; remoteParticipants?: VideoGalleryRemoteParticipant[]; diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index b2c11581267..349358839e3 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -65,10 +65,10 @@ export type { /* @conditional-compile-remove(together-mode) */ export type { TogetherModeStreamViewResult, - TogetherModeVideoStreamProp, - TogetherModeStreamsProp, - TogetherModeParticipantSeatingProp, - TogetherModeSeatingPositionProp + TogetherModeStream, + VideoGalleryTogetherModeParticipantPosition, + VideoGalleryTogetherModeSeatingInfo, + VideoGalleryTogetherModeStreams } from './types'; export type { RaisedHand } from './types'; @@ -94,8 +94,5 @@ export type { SurveyIssuesHeadingStrings } from './types'; export type { CallSurveyImprovementSuggestions } from './types'; -/* @conditional-compile-remove(together-mode) */ -export type { TogetherModeStreamViewResult } from './types'; - /* @conditional-compile-remove(media-access) */ export type { MediaAccess } from './types'; diff --git a/packages/react-composites/src/composites/CallComposite/components/Prompt.tsx b/packages/react-composites/src/composites/CallComposite/components/Prompt.tsx index 2440a24b0a8..8fd0d975974 100644 --- a/packages/react-composites/src/composites/CallComposite/components/Prompt.tsx +++ b/packages/react-composites/src/composites/CallComposite/components/Prompt.tsx @@ -142,5 +142,5 @@ export interface SpotlightPromptStrings { /** * Label for button to close prompt */ - closeSpotlightPromptButtonLabel: string; + closeSpotlightPromptButtonLabel?: string; } diff --git a/packages/react-composites/src/composites/localization/locales/en-US/strings.json b/packages/react-composites/src/composites/localization/locales/en-US/strings.json index 1f0824b16d3..c1560dfbad8 100644 --- a/packages/react-composites/src/composites/localization/locales/en-US/strings.json +++ b/packages/react-composites/src/composites/localization/locales/en-US/strings.json @@ -235,7 +235,7 @@ "moreButtonGalleryDefaultLayoutLabel": "Gallery view", "moreButtonGalleryFocusedContentLayoutLabel": "Focus on content", "moreButtonLargeGalleryDefaultLayoutLabel": "Large Gallery", - "moreButtonGalleryTogetherModeLayoutLabel": "Together mode", + "moreButtonTogetherModeLayoutLabel": "Together mode", "capabilityChangedNotification": { "turnVideoOn": { "lostDueToMeetingOption": "Your camera has been disabled. You can no longer share video.", From 1abe02694899494ef34fdc4bc56a4b504c83d85f Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Tue, 12 Nov 2024 00:53:38 +0000 Subject: [PATCH 09/37] API cleanup --- .../src/handlers/createCommonHandlers.ts | 2 +- .../src/StatefulCallClient.ts | 49 +++---------------- .../review/beta/communication-react.api.md | 8 +-- .../src/components/MeetingReactionOverlay.tsx | 1 + .../src/components/TogetherModeOverlay.tsx | 1 + .../VideoGallery/TogetherModeStream.tsx | 6 ++- .../CallComposite/components/MediaGallery.tsx | 7 +-- .../CallComposite/selectors/baseSelectors.ts | 12 ++++- .../common/ControlBar/DesktopMoreButton.tsx | 12 +++-- 9 files changed, 39 insertions(+), 59 deletions(-) diff --git a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts index 9cdce5d4337..941f21a23b0 100644 --- a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts +++ b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts @@ -801,7 +801,7 @@ export const createDefaultCommonCallingHandlers = memoizeOne( } if (togetherModeStreams.mainVideoStream.view) { - callClient.disposeView(call.id, togetherModeStreams.mainVideoStream); + callClient.disposeView(call.id, undefined, togetherModeStreams.mainVideoStream); } }; /* @conditional-compile-remove(together-mode) */ diff --git a/packages/calling-stateful-client/src/StatefulCallClient.ts b/packages/calling-stateful-client/src/StatefulCallClient.ts index 991ca5e08ea..a33e6aad7e5 100644 --- a/packages/calling-stateful-client/src/StatefulCallClient.ts +++ b/packages/calling-stateful-client/src/StatefulCallClient.ts @@ -117,7 +117,10 @@ export interface StatefulCallClient extends CallClient { createView( callId: string | undefined, participantId: CommunicationIdentifier | undefined, - stream: LocalVideoStreamState | RemoteVideoStreamState, + stream: + | LocalVideoStreamState + | RemoteVideoStreamState + | /* @conditional-compile-remove(together-mode) */ CallFeatureStreamState, options?: CreateViewOptions ): Promise; /** @@ -147,47 +150,11 @@ export interface StatefulCallClient extends CallClient { disposeView( callId: string | undefined, participantId: CommunicationIdentifier | undefined, - stream: LocalVideoStreamState | RemoteVideoStreamState + stream: + | LocalVideoStreamState + | RemoteVideoStreamState + | /* @conditional-compile-remove(together-mode) */ CallFeatureStreamState ): void; - - /* @conditional-compile-remove(together-mode) */ - /** - * Renders a {@link CallFeatureStreamState} - * {@link VideoStreamRendererViewState} under the relevant {@link CallFeatureStreamState} - * {@link @azure/communication-calling#VideoStreamRenderer.createView}. - * - * Scenario 1: Render CallFeatureStreamState - * - CallId is required and stream of type CallFeatureStreamState is required - * - Resulting {@link VideoStreamRendererViewState} is stored in the given callId and participantId in - * {@link CallClientState} - * - * @param callId - CallId for the given stream. Can be undefined if the stream is not part of any call. - * @param stream - The LocalVideoStreamState or RemoteVideoStreamState to start rendering. - * @param options - Options that are passed to the {@link @azure/communication-calling#VideoStreamRenderer}. - * @beta - */ - createView( - callId: string, - stream: CallFeatureStreamState, - options?: CreateViewOptions - ): Promise; - - /* @conditional-compile-remove(together-mode) */ - /** - * Stops rendering a {@link CallFeatureStreamState} and removes the - * {@link VideoStreamRendererView} from the relevant {@link CallFeatureStreamState} in {@link CallClientState} or - * {@link @azure/communication-calling#VideoStreamRenderer.dispose}. - * - * Its important to disposeView to clean up resources properly. - * - * Scenario 1: Dispose CallFeatureStreamState - * - CallId is required and stream of type CallFeatureStreamState is required - * - * @param callId - CallId for the given stream. Can be undefined if the stream is not part of any call. - * @param stream - The LocalVideoStreamState or RemoteVideoStreamState to dispose. - * @beta - */ - disposeView(callId: string, stream: CallFeatureStreamState): void; /** * The CallAgent is used to handle calls. * To create the CallAgent, pass a CommunicationTokenCredential object provided from SDK. diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index 878f7591386..17cbe584713 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -4837,12 +4837,8 @@ export type StartTeamsCallIdentifier = MicrosoftTeamsUserIdentifier | PhoneNumbe export interface StatefulCallClient extends CallClient { createCallAgent(...args: Parameters): Promise; createTeamsCallAgent(...args: Parameters): Promise; - createView(callId: string | undefined, participantId: CommunicationIdentifier | undefined, stream: LocalVideoStreamState | RemoteVideoStreamState, options?: CreateViewOptions): Promise; - // @beta - createView(callId: string, stream: CallFeatureStreamState, options?: CreateViewOptions): Promise; - disposeView(callId: string | undefined, participantId: CommunicationIdentifier | undefined, stream: LocalVideoStreamState | RemoteVideoStreamState): void; - // @beta - disposeView(callId: string, stream: CallFeatureStreamState): void; + createView(callId: string | undefined, participantId: CommunicationIdentifier | undefined, stream: LocalVideoStreamState | RemoteVideoStreamState | /* @conditional-compile-remove(together-mode) */ CallFeatureStreamState, options?: CreateViewOptions): Promise; + disposeView(callId: string | undefined, participantId: CommunicationIdentifier | undefined, stream: LocalVideoStreamState | RemoteVideoStreamState | /* @conditional-compile-remove(together-mode) */ CallFeatureStreamState): void; getState(): CallClientState; offStateChange(handler: (state: CallClientState) => void): void; onStateChange(handler: (state: CallClientState) => void): void; diff --git a/packages/react-components/src/components/MeetingReactionOverlay.tsx b/packages/react-components/src/components/MeetingReactionOverlay.tsx index e59a7d6d7d9..36ac9430797 100644 --- a/packages/react-components/src/components/MeetingReactionOverlay.tsx +++ b/packages/react-components/src/components/MeetingReactionOverlay.tsx @@ -137,6 +137,7 @@ export const MeetingReactionOverlay = (props: MeetingReactionOverlayProps): JSX. return (
{containerWidth && containerHeight && ( @@ -95,8 +98,9 @@ export const TogetherModeStream = React.memo( >
{reactionResources && ( { const VideoGalleryMemoized = useMemo(() => { const layoutBasedOnUserSelection = (): VideoGalleryLayout => { - /* @conditional-compile-remove(together-mode) */ - if (videoGalleryProps?.isTogetherModeActive) { - return 'togetherMode'; - } else { - return props.localVideoTileOptions ? layoutBasedOnTilePosition : props.userSetGalleryLayout; - } + return props.localVideoTileOptions ? layoutBasedOnTilePosition : props.userSetGalleryLayout; return layoutBasedOnTilePosition; }; diff --git a/packages/react-composites/src/composites/CallComposite/selectors/baseSelectors.ts b/packages/react-composites/src/composites/CallComposite/selectors/baseSelectors.ts index 0690061a3df..48e0e308d95 100644 --- a/packages/react-composites/src/composites/CallComposite/selectors/baseSelectors.ts +++ b/packages/react-composites/src/composites/CallComposite/selectors/baseSelectors.ts @@ -42,7 +42,8 @@ import { CommunicationIdentifier } from '@azure/communication-common'; import { CaptionsKind } from '@azure/communication-calling'; import { ReactionResources } from '@internal/react-components'; - +/* @conditional-compile-remove(together-mode) */ +import { CommunicationIdentifierKind } from '@azure/communication-common'; /** * @private */ @@ -317,3 +318,12 @@ export const getVideoBackgroundImages = (state: CallAdapterState): VideoBackgrou */ export const getIsTogetherModeActive = (state: CallAdapterState): boolean | undefined => state.call?.togetherMode.isActive; + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + * Gets the together mode streams state. + * @param state - The current state of the call adapter. + * @returns The together mode streams state or undefined. + */ +export const getLocalUserId = (state: CallAdapterState): CommunicationIdentifierKind | undefined => state.userId; diff --git a/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx b/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx index fb81d750298..3ddd02f5f59 100644 --- a/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx +++ b/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx @@ -27,7 +27,7 @@ import { showDtmfDialer } from '../../CallComposite/utils/MediaGalleryUtils'; import { useSelector } from '../../CallComposite/hooks/useSelector'; import { getTargetCallees } from '../../CallComposite/selectors/baseSelectors'; /* @conditional-compile-remove(together-mode) */ -import { getIsTogetherModeActive, getLatestCapabilitiesChangedInfo } from '../../CallComposite/selectors/baseSelectors'; +import { getIsTogetherModeActive, getCapabilites, getLocalUserId } from '../../CallComposite/selectors/baseSelectors'; import { getTeamsMeetingCoordinates, getIsTeamsMeeting } from '../../CallComposite/selectors/baseSelectors'; import { CallControlOptions } from '../../CallComposite'; @@ -77,7 +77,9 @@ export const DesktopMoreButton = (props: DesktopMoreButtonProps): JSX.Element => /* @conditional-compile-remove(together-mode) */ const isTogetherModeActive = useSelector(getIsTogetherModeActive); /* @conditional-compile-remove(together-mode) */ - const latestCapabilities = useSelector(getLatestCapabilitiesChangedInfo); + const participantCapability = useSelector(getCapabilites); + /* @conditional-compile-remove(together-mode) */ + const participantId = useSelector(getLocalUserId); const [dtmfDialerChecked, setDtmfDialerChecked] = useState(props.dtmfDialerPresent ?? false); @@ -396,7 +398,11 @@ export const DesktopMoreButton = (props: DesktopMoreButtonProps): JSX.Element => /* @conditional-compile-remove(overflow-top-composite) */ galleryOptions.subMenuProps?.items?.push(overflowGalleryOption); /* @conditional-compile-remove(together-mode) */ - if (latestCapabilities?.newValue.startTogetherMode?.isPresent || isTogetherModeActive) { + if ( + // Only Teams User should be able to start together mode + (participantId?.kind === 'microsoftTeamsUser' && participantCapability?.startTogetherMode?.isPresent) || + isTogetherModeActive + ) { galleryOptions.subMenuProps?.items?.push(togetherModeOption); } if (props.callControls === true || (props.callControls as CallControlOptions)?.galleryControlsButton !== false) { From 52843b53d839d3208199df9d99e85cbe1b7117e1 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Tue, 12 Nov 2024 20:30:10 +0000 Subject: [PATCH 10/37] Updated together mode reactions --- .../src/TogetherModeSubscriber.ts | 12 +- .../src/components/TogetherModeOverlay.tsx | 279 +++++++++--------- .../VideoGallery/TogetherModeStream.tsx | 10 +- 3 files changed, 153 insertions(+), 148 deletions(-) diff --git a/packages/calling-stateful-client/src/TogetherModeSubscriber.ts b/packages/calling-stateful-client/src/TogetherModeSubscriber.ts index 1982c8ce631..198a53f3880 100644 --- a/packages/calling-stateful-client/src/TogetherModeSubscriber.ts +++ b/packages/calling-stateful-client/src/TogetherModeSubscriber.ts @@ -82,12 +82,12 @@ export class TogetherModeSubscriber { ): void => { for (const stream of removedStreams) { this._togetherModeVideoStreamSubscribers.get(stream.id)?.unsubscribe(); - disposeView( - this._context, - this._internalContext, - this._callIdRef.callId, - convertSdkCallFeatureStreamToDeclarativeCallFeatureStream(stream, this._featureName) - ); + // disposeView( + // this._context, + // this._internalContext, + // this._callIdRef.callId, + // convertSdkCallFeatureStreamToDeclarativeCallFeatureStream(stream, this._featureName) + // ); this._internalContext.deleteCallFeatureRenderInfo( this._callIdRef.callId, this._featureName, diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index b9cc8031ddc..39662bcd922 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -2,26 +2,28 @@ // Licensed under the MIT License. /* @conditional-compile-remove(together-mode) */ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; /* @conditional-compile-remove(together-mode) */ import { Reaction, ReactionResources, VideoGalleryTogetherModeParticipantPosition, VideoGalleryLocalParticipant, - VideoGalleryRemoteParticipant + VideoGalleryRemoteParticipant, + VideoGalleryTogetherModeSeatingInfo } from '../types'; /* @conditional-compile-remove(together-mode) */ import { getReactionStyleBucket, IReactionStyleBucket, ITogetherModeReactionStyleBucket, - opacityAnimationStyles, + moveAnimationStyles, + // opacityAnimationStyles, spriteAnimationStyles } from './styles/ReactionOverlay.style'; /* @conditional-compile-remove(together-mode) */ import { - getCombinedKey, + // getCombinedKey, REACTION_NUMBER_OF_ANIMATION_FRAMES, REACTION_START_DISPLAY_SIZE } from './VideoGallery/utils/reactionUtils'; @@ -45,31 +47,21 @@ import { _HighContrastAwareIcon } from './HighContrastAwareIcon'; * @internal */ type VisibleTogetherModeReaction = { - reaction: Reaction; - id: string; - styleBucket: ITogetherModeReactionStyleBucket; + reaction?: Reaction; + isHandRaised?: boolean; + isSpotlighted?: boolean; + isMuted?: boolean; + id?: string; + styleBucket?: ITogetherModeReactionStyleBucket; displayName?: string; + showDisplayName?: boolean; }; -// /** -// * Reaction overlay component props -// * -// * Can be used with {@link VideoTile}. -// * -// * @internal -// */ -// type VisibleSignaling = { -// isHandRaised?: boolean; -// isSpotlighted?: boolean; -// styleBucket: ITogetherModeReactionStyleBucket; -// displayName?: string; -// }; - /* @conditional-compile-remove(together-mode) */ -type ReceivedReaction = { - id: string; - status: 'animating' | 'completedAnimating' | 'ignored'; -}; +// type ReceivedReaction = { +// id: string; +// status: 'animating' | 'completedAnimating' | 'ignored'; +// }; /* @conditional-compile-remove(together-mode) */ /** @@ -88,66 +80,81 @@ export const TogetherModeOverlay = React.memo( const locale = useLocale(); const { reactionResources, remoteParticipants, localParticipant, participantsSeatingArrangement } = props; // Reactions that are currently being animated - const [visibleReactions, setVisibleReactions] = useState([]); - // const [pinDisplayName, setPinDisplayName] = useState>({}); + // const [visibleReactions, setVisibleReactions] = useState([]); + const [visibleSignals, setVisibleSignals] = useState>({}); // Dictionary of userId to a reaction status. This is used to track the latest received reaction // per user to avoid animating the same reaction multiple times and to limit the number of // active reactions of a certain type. - const latestReceivedReaction = useRef>({}); + // const latestReceivedReaction = useRef>({}); - const particpantsReactions: Reaction[] = useMemo(() => { - const reactions = - remoteParticipants - ?.map((remoteParticipant) => remoteParticipant.reaction) - .filter((reaction): reaction is Reaction => !!reaction) ?? []; - if (localParticipant?.reaction) { - reactions.push(localParticipant.reaction); + const participantsSignals: Record = useMemo(() => { + const signals: Record = {}; + remoteParticipants?.map((particpant) => { + const participantID = particpant.userId; + const togetherModeSeatStyle: ITogetherModeReactionStyleBucket = { + sizeScale: 0.9, + opacityMax: 0.9, + width: participantsSeatingArrangement?.[participantID]?.width ?? 0, + height: participantsSeatingArrangement?.[participantID]?.height ?? 0, + left: participantsSeatingArrangement?.[participantID]?.left ?? 0, + top: participantsSeatingArrangement?.[participantID]?.top ?? 0 + }; + signals[participantID] = { + id: participantID, + reaction: particpant.reaction, + isHandRaised: !!particpant.raisedHand, + isSpotlighted: !!particpant.spotlight, + isMuted: particpant.isMuted, + displayName: !particpant?.displayName + ? locale.strings.videoGallery.displayNamePlaceholder + : particpant?.displayName, + styleBucket: togetherModeSeatStyle + }; + }); + if (localParticipant) { + const togetherModeSeatStyle: ITogetherModeReactionStyleBucket = { + sizeScale: 0.9, + opacityMax: 0.9, + width: participantsSeatingArrangement?.[localParticipant.userId]?.width ?? 0, + height: participantsSeatingArrangement?.[localParticipant.userId]?.height ?? 0, + left: participantsSeatingArrangement?.[localParticipant.userId]?.left ?? 0, + top: participantsSeatingArrangement?.[localParticipant.userId]?.top ?? 0 + }; + signals[localParticipant.userId] = { + id: localParticipant.userId, + reaction: localParticipant.reaction, + isHandRaised: !!localParticipant.raisedHand, + isSpotlighted: !!localParticipant.spotlight, + isMuted: localParticipant.isMuted, + displayName: !localParticipant?.displayName + ? locale.strings.videoGallery.displayNamePlaceholder + : localParticipant?.displayName, + styleBucket: togetherModeSeatStyle + }; } - return reactions; - }, [remoteParticipants, localParticipant]); - - // const participantsRaiseHand: Record = useMemo(() => { - // const raiseHandParticipants: Record = {}; - // remoteParticipants?.forEach((participant) => { - // if (participant.raisedHand) { - // raiseHandParticipants[participant.userId] = true; - // } - // }); - // if (localParticipant?.raisedHand) { - // raiseHandParticipants[localParticipant.userId] = true; - // } - // return raiseHandParticipants; - // }, [localParticipant, remoteParticipants]); - - // const participantsSpotlight: Record = useMemo(() => { - // const spotlightParticipants: Record = {}; - // remoteParticipants?.forEach((participant) => { - // if (participant.spotlight) { - // spotlightParticipants[participant.userId] = true; - // } - // }); - // if (localParticipant?.spotlight) { - // spotlightParticipants[localParticipant.userId] = true; - // } - // return spotlightParticipants; - // }, [localParticipant, remoteParticipants]); + return signals; + }, [ + remoteParticipants, + localParticipant, + locale.strings.videoGallery.displayNamePlaceholder, + participantsSeatingArrangement + ]); - const updateVisibleReactions = useCallback( + const updateTogetherModeSeatingUI = useCallback( ( - reaction: Reaction, - userId: string, - displayName: string, - seatingPosition: { width: number; height: number; top: number; left: number } + participant: VideoGalleryLocalParticipant | VideoGalleryRemoteParticipant, + seatingPosition: VideoGalleryTogetherModeSeatingInfo ): void => { - const combinedKey = getCombinedKey(userId, reaction.reactionType, reaction.receivedOn); + // const combinedKey = getCombinedKey(userId, reaction.reactionType, reaction.receivedOn); - const alreadyHandled = latestReceivedReaction.current[userId]?.id === combinedKey; - if (alreadyHandled) { - return; - } + // const alreadyHandled = latestReceivedReaction.current[userId]?.id === combinedKey; + // if (alreadyHandled) { + // return; + // } - const reactionStyle: ITogetherModeReactionStyleBucket = { + const participantID = participant.userId; + const togetherModeSeatStyle: ITogetherModeReactionStyleBucket = { sizeScale: 0.9, opacityMax: 0.9, width: seatingPosition.width, @@ -156,62 +163,52 @@ export const TogetherModeOverlay = React.memo( top: seatingPosition.top }; - latestReceivedReaction.current[userId] = { - id: combinedKey, - status: 'animating' - }; - - setVisibleReactions([ - ...visibleReactions, - { - reaction: reaction, - id: combinedKey, - displayName: displayName, - styleBucket: reactionStyle + // removeVisibleSignalinDiv(participantID); + setVisibleSignals((prevVisibleSignals) => ({ + ...prevVisibleSignals, + [participant.userId]: { + id: participantID, + reaction: participant.reaction, + isHandRaised: !!participant.raisedHand, + isSpotlighted: !!participant.spotlight, + isMuted: participant.isMuted, + displayName: participant.displayName || locale.strings.videoGallery.displayNamePlaceholder, + showDisplayName: !!( + participant.isMuted || + participant.spotlight || + participant.raisedHand || + participant.reaction + ), + styleBucket: togetherModeSeatStyle } - ]); + })); return; }, - [visibleReactions] + [locale.strings.videoGallery.displayNamePlaceholder] ); - const removeVisibleReaction = (reactionType: string, id: string): void => { - setVisibleReactions(visibleReactions.filter((reaction) => reaction.id !== id)); - Object.entries(latestReceivedReaction.current).forEach(([userId, reaction]) => { - const userLastReaction = latestReceivedReaction.current[userId]; - if (reaction.id === id && userLastReaction) { - userLastReaction.status = 'completedAnimating'; - } - }); - }; + // const removeVisibleSignalinDiv = (id: string): void => { + // document.getElementById(id)?.remove(); + // }; // Update visible reactions when remote participants send a reaction useEffect(() => { remoteParticipants?.map((participant) => { - if (participant?.reaction && participant.videoStream?.isAvailable) { + if (participant.videoStream?.isAvailable) { const seatingPosition = participantsSeatingArrangement?.[participant.userId]; - const displayName = !participant?.displayName - ? locale.strings.videoGallery.displayNamePlaceholder - : participant?.displayName; - seatingPosition && - updateVisibleReactions(participant.reaction, participant.userId, displayName, seatingPosition); + seatingPosition && updateTogetherModeSeatingUI(participant, seatingPosition); } }); - if (localParticipant?.reaction && localParticipant.videoStream?.isAvailable) { + if (localParticipant && localParticipant.videoStream?.isAvailable) { const seatingPosition = participantsSeatingArrangement?.[localParticipant.userId]; - const displayName = !localParticipant?.displayName - ? locale.strings.videoGallery.displayNamePlaceholder - : localParticipant?.displayName; - seatingPosition && - updateVisibleReactions(localParticipant.reaction, localParticipant.userId, displayName, seatingPosition); + seatingPosition && updateTogetherModeSeatingUI(localParticipant, seatingPosition); } }, [ - locale.strings.videoGallery.displayNamePlaceholder, - particpantsReactions, remoteParticipants, localParticipant, - updateVisibleReactions, - participantsSeatingArrangement + participantsSeatingArrangement, + updateTogetherModeSeatingUI, + participantsSignals ]); const styleBucket = (): IReactionStyleBucket => getReactionStyleBucket(); @@ -222,15 +219,15 @@ export const TogetherModeOverlay = React.memo( backgroundColor: 'transparent' })} > - {visibleReactions.map((reaction) => ( + {Object.values(visibleSignals).map((participantSignal) => (
@@ -242,33 +239,49 @@ export const TogetherModeOverlay = React.memo( // Fourth div - Play Animation as the other animation applies on the base play animation for the sprite
{ - removeVisibleReaction(reaction.reaction.reactionType, reaction.id); - }} - style={opacityAnimationStyles(reaction.styleBucket.opacityMax)} + style={moveAnimationStyles( + (participantSignal.styleBucket?.height ?? 0) / 2, // dividing by two because reactionOverlayStyle height is set to 50% + ((participantSignal.styleBucket?.height ?? 0) / 2) * (1 - 0.7 * 0.95) + )} >
} - {/*
-
- <_HighContrastAwareIcon disabled={false} iconName="ControlButtonRaiseHand" /> -
-
{reaction.displayName}
-
- <_HighContrastAwareIcon iconName={'Muted'} /> - <_HighContrastAwareIcon iconName={'ControlButtonExitSpotlight'} /> +
+
+
+ {participantSignal.isHandRaised && ( + <_HighContrastAwareIcon disabled={true} iconName="ControlButtonRaiseHand" /> + )} + {participantSignal.showDisplayName && participantSignal.displayName} + {participantSignal.isMuted && <_HighContrastAwareIcon disabled={true} iconName={'Muted'} />} + {participantSignal.isSpotlighted && ( + <_HighContrastAwareIcon disabled={true} iconName={'ControlButtonExitSpotlight'} /> + )} +
-
*/} +
))} diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx index 0636a911828..2b324b79835 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx @@ -49,7 +49,6 @@ export const TogetherModeStream = React.memo( isTogetherModeActive, onCreateTogetherModeStreamView, onStartTogetherMode, - onDisposeTogetherModeStreamViews, onSetTogetherModeSceneSize, togetherModeStreams, seatingCoordinates, @@ -75,15 +74,8 @@ export const TogetherModeStream = React.memo( onSetTogetherModeSceneSize(containerWidth, containerHeight); }, [onSetTogetherModeSceneSize, containerWidth, containerHeight]); - useEffect(() => { - return () => { - !togetherModeStreams?.mainVideoStream?.isAvailable && - onDisposeTogetherModeStreamViews && - onDisposeTogetherModeStreamViews(); - }; - }, [onDisposeTogetherModeStreamViews, togetherModeStreams]); const stream = togetherModeStreams?.mainVideoStream; - const showLoadingIndicator = stream && !(stream.isAvailable && stream.isReceiving); + const showLoadingIndicator = stream && stream.isAvailable && stream.isReceiving; return ( <> From 514f5e0493840222ccf71e70f866ba433daf2f17 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Sat, 16 Nov 2024 21:35:53 +0000 Subject: [PATCH 11/37] make together mode button disabled for ACS user if its not started by a Teams User --- .../src/TogetherModeSubscriber.ts | 8 -------- .../common/ControlBar/DesktopMoreButton.tsx | 12 +++++------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/calling-stateful-client/src/TogetherModeSubscriber.ts b/packages/calling-stateful-client/src/TogetherModeSubscriber.ts index 198a53f3880..1a387d8be69 100644 --- a/packages/calling-stateful-client/src/TogetherModeSubscriber.ts +++ b/packages/calling-stateful-client/src/TogetherModeSubscriber.ts @@ -15,8 +15,6 @@ import { CallIdRef } from './CallIdRef'; /* @conditional-compile-remove(together-mode) */ import { InternalCallContext } from './InternalCallContext'; /* @conditional-compile-remove(together-mode) */ -import { disposeView } from './CallFeatureStreamUtils'; -/* @conditional-compile-remove(together-mode) */ import { convertSdkCallFeatureStreamToDeclarativeCallFeatureStream } from './Converter'; /* @conditional-compile-remove(together-mode) */ import { TogetherModeVideoStreamSubscriber } from './TogetherModeVideoStreamSubscriber'; @@ -82,12 +80,6 @@ export class TogetherModeSubscriber { ): void => { for (const stream of removedStreams) { this._togetherModeVideoStreamSubscribers.get(stream.id)?.unsubscribe(); - // disposeView( - // this._context, - // this._internalContext, - // this._callIdRef.callId, - // convertSdkCallFeatureStreamToDeclarativeCallFeatureStream(stream, this._featureName) - // ); this._internalContext.deleteCallFeatureRenderInfo( this._callIdRef.callId, this._featureName, diff --git a/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx b/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx index 3ddd02f5f59..97885c4ab97 100644 --- a/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx +++ b/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx @@ -361,6 +361,10 @@ export const DesktopMoreButton = (props: DesktopMoreButtonProps): JSX.Element => props.onUserSetGalleryLayout && props.onUserSetGalleryLayout('togetherMode'); setFocusedContentOn(false); }, + disabled: !( + (participantId?.kind === 'microsoftTeamsUser' && participantCapability?.startTogetherMode?.isPresent) || + isTogetherModeActive + ), iconProps: { iconName: 'LargeGalleryLayout', styles: { root: { lineHeight: 0 } } @@ -398,13 +402,7 @@ export const DesktopMoreButton = (props: DesktopMoreButtonProps): JSX.Element => /* @conditional-compile-remove(overflow-top-composite) */ galleryOptions.subMenuProps?.items?.push(overflowGalleryOption); /* @conditional-compile-remove(together-mode) */ - if ( - // Only Teams User should be able to start together mode - (participantId?.kind === 'microsoftTeamsUser' && participantCapability?.startTogetherMode?.isPresent) || - isTogetherModeActive - ) { - galleryOptions.subMenuProps?.items?.push(togetherModeOption); - } + galleryOptions.subMenuProps?.items?.push(togetherModeOption); if (props.callControls === true || (props.callControls as CallControlOptions)?.galleryControlsButton !== false) { moreButtonContextualMenuItems.push(galleryOptions); } From fb69100204d04ab3ff5ca271f07c677e14be0bdd Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Wed, 27 Nov 2024 21:15:16 +0000 Subject: [PATCH 12/37] Together mode with raiseHand, spotlight and mute particpant Implementation --- .../src/handlers/createCommonHandlers.ts | 13 +- .../src/videoGallerySelector.ts | 4 +- .../review/beta/communication-react.api.md | 26 +- packages/communication-react/src/index.ts | 3 +- .../src/components/TogetherModeOverlay.tsx | 262 ++++++------------ .../src/components/VideoGallery.tsx | 107 ++++--- .../src/components/VideoGallery/Layout.ts | 14 +- .../VideoGallery/TogetherModeLayout.tsx | 127 ++++++++- .../VideoGallery/TogetherModeStream.tsx | 89 +++--- .../utils/videoGalleryLayoutUtils.ts | 5 +- .../styles/ReactionOverlay.style.ts | 13 - .../components/styles/TogetherMode.styles.ts | 113 ++++++++ packages/react-components/src/index.ts | 3 +- .../src/types/TogetherModeTypes.ts | 10 +- .../CallComposite/MockCallAdapter.ts | 6 +- .../adapter/AzureCommunicationCallAdapter.ts | 14 +- .../CallComposite/adapter/CallAdapter.ts | 6 +- .../CallComposite/hooks/useHandlers.ts | 6 +- .../AzureCommunicationCallWithChatAdapter.ts | 12 +- .../adapter/CallWithChatAdapter.ts | 6 +- .../adapter/CallWithChatBackedCallAdapter.ts | 12 +- .../tests/app/lib/MockCallAdapter.ts | 6 +- 22 files changed, 488 insertions(+), 369 deletions(-) create mode 100644 packages/react-components/src/components/styles/TogetherMode.styles.ts diff --git a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts index 941f21a23b0..ccf70d21a22 100644 --- a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts +++ b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts @@ -41,7 +41,7 @@ import { TeamsCaptions } from '@azure/communication-calling'; import { Reaction } from '@azure/communication-calling'; import { _ComponentCallingHandlers } from './createHandlers'; /* @conditional-compile-remove(together-mode) */ -import { TogetherModeStreamViewResult } from '@internal/react-components'; +import { TogetherModeStreamViewResult, TogetherModeStreamOptions } from '@internal/react-components'; /** * Object containing all the handlers required for calling components. * @@ -110,7 +110,7 @@ export interface CommonCallingHandlers { * * @beta */ - onCreateTogetherModeStreamView: (options?: VideoStreamOptions) => Promise; + onCreateTogetherModeStreamView: (options?: TogetherModeStreamOptions) => Promise; /* @conditional-compile-remove(together-mode) */ /** @@ -132,7 +132,7 @@ export interface CommonCallingHandlers { * * @beta */ - onDisposeTogetherModeStreamViews: () => Promise; + onDisposeTogetherModeStreamView: () => Promise; /* @conditional-compile-remove(media-access) */ onForbidParticipantAudio?: (userIds: string[]) => Promise; /* @conditional-compile-remove(media-access) */ @@ -760,7 +760,7 @@ export const createDefaultCommonCallingHandlers = memoizeOne( : undefined; /* @conditional-compile-remove(together-mode) */ const onCreateTogetherModeStreamView = async ( - options = { scalingMode: 'Fit', isMirrored: false } as VideoStreamOptions + options = { scalingMode: 'Fit', isMirrored: false, viewKind: 'main' } as TogetherModeStreamOptions ): Promise => { if (!call) { return; @@ -774,7 +774,6 @@ export const createDefaultCommonCallingHandlers = memoizeOne( const mainVideoStream = togetherModeStreams.mainVideoStream; if (mainVideoStream && mainVideoStream.isAvailable && !mainVideoStream.view) { - // const createViewResult = await callClient.createView(call.id, mainVideoStream, options); const createViewResult = await callClient.createView(call.id, undefined, mainVideoStream, options); // SDK currently only supports 1 Video media stream type togetherModeCreateViewResult.mainVideoView = createViewResult?.view @@ -785,7 +784,7 @@ export const createDefaultCommonCallingHandlers = memoizeOne( }; /* @conditional-compile-remove(together-mode) */ - const onDisposeTogetherModeStreamViews = async (): Promise => { + const onDisposeTogetherModeStreamView = async (): Promise => { if (!call) { return; } @@ -905,7 +904,7 @@ export const createDefaultCommonCallingHandlers = memoizeOne( /* @conditional-compile-remove(together-mode) */ onSetTogetherModeSceneSize, /* @conditional-compile-remove(together-mode) */ - onDisposeTogetherModeStreamViews, + onDisposeTogetherModeStreamView, /* @conditional-compile-remove(media-access) */ onForbidParticipantAudio, /* @conditional-compile-remove(media-access) */ diff --git a/packages/calling-component-bindings/src/videoGallerySelector.ts b/packages/calling-component-bindings/src/videoGallerySelector.ts index 0bcc312f9e8..fc885ca6909 100644 --- a/packages/calling-component-bindings/src/videoGallerySelector.ts +++ b/packages/calling-component-bindings/src/videoGallerySelector.ts @@ -58,7 +58,7 @@ export type VideoGallerySelector = ( /* @conditional-compile-remove(together-mode) */ isTogetherModeActive?: boolean; /* @conditional-compile-remove(together-mode) */ - canStartTogetherMode?: boolean; + startTogetherModeEnabled?: boolean; /* @conditional-compile-remove(together-mode) */ togetherModeStreamsMap?: TogetherModeStreamsState; /* @conditional-compile-remove(together-mode) */ @@ -172,7 +172,7 @@ export const videoGallerySelector: VideoGallerySelector = createSelector( /* @conditional-compile-remove(together-mode) */ isTogetherModeActive: togetherModeCallFeature?.isActive, /* @conditional-compile-remove(together-mode) */ - canStartTogetherMode: capabilities?.startTogetherMode.isPresent + startTogetherModeEnabled: capabilities?.startTogetherMode.isPresent }; } ); diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index 42ba4fcf65e..2c5d13104f6 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -444,14 +444,14 @@ export interface CallAdapterCallOperations { allowUnsupportedBrowserVersion(): void; createStreamView(remoteUserId?: string, options?: VideoStreamOptions): Promise; // @beta - createTogetherModeStreamViews(options?: VideoStreamOptions): Promise; + createTogetherModeStreamView(options?: TogetherModeStreamOptions): Promise; disposeLocalVideoStreamView(): Promise; disposeRemoteVideoStreamView(remoteUserId: string): Promise; disposeScreenShareStreamView(remoteUserId: string): Promise; // @deprecated disposeStreamView(remoteUserId?: string, options?: VideoStreamOptions): Promise; // @beta - disposeTogetherModeStreamViews(): Promise; + disposeTogetherModeStreamView(): Promise; holdCall(): Promise; leaveCall(forEveryone?: boolean): Promise; lowerHand(): Promise; @@ -1216,7 +1216,7 @@ export interface CallWithChatAdapterManagement { askDevicePermission(constrain: PermissionConstraints): Promise; createStreamView(remoteUserId?: string, options?: VideoStreamOptions): Promise; // @beta - createTogetherModeStreamViews(options?: VideoStreamOptions): Promise; + createTogetherModeStreamView(options?: TogetherModeStreamOptions): Promise; // @beta deleteImage(imageId: string): Promise; deleteMessage(messageId: string): Promise; @@ -1225,7 +1225,7 @@ export interface CallWithChatAdapterManagement { disposeScreenShareStreamView(remoteUserId: string): Promise; disposeStreamView(remoteUserId?: string, options?: VideoStreamOptions): Promise; // @beta - disposeTogetherModeStreamViews(): Promise; + disposeTogetherModeStreamView(): Promise; // (undocumented) downloadResourceToCache(resourceDetails: ResourceDetails): Promise; fetchInitialData(): Promise; @@ -2203,7 +2203,7 @@ export interface CommonCallingHandlers { // (undocumented) onCreateRemoteStreamView: (userId: string, options?: VideoStreamOptions) => Promise; // @beta - onCreateTogetherModeStreamView: (options?: VideoStreamOptions) => Promise; + onCreateTogetherModeStreamView: (options?: TogetherModeStreamOptions) => Promise; // (undocumented) onDisposeLocalScreenShareStreamView: () => Promise; // (undocumented) @@ -2215,7 +2215,7 @@ export interface CommonCallingHandlers { // (undocumented) onDisposeRemoteVideoStreamView: (userId: string) => Promise; // @beta - onDisposeTogetherModeStreamViews: () => Promise; + onDisposeTogetherModeStreamView: () => Promise; // (undocumented) onForbidParticipantAudio?: (userIds: string[]) => Promise; // (undocumented) @@ -5070,6 +5070,12 @@ export interface TogetherModeStream { }; } +// @beta +export interface TogetherModeStreamOptions extends VideoStreamOptions { + // (undocumented) + viewKind?: 'main' | 'panoramic'; +} + // @beta export interface TogetherModeStreamsState { // (undocumented) @@ -5324,8 +5330,6 @@ export type VideoGalleryParticipant = { // @public export interface VideoGalleryProps { - // (undocumented) - canStartTogetherMode?: boolean; dominantSpeakers?: string[]; // (undocumented) isTogetherModeActive?: boolean; @@ -5347,7 +5351,7 @@ export interface VideoGalleryProps { onDisposeRemoteStreamView?: (userId: string) => Promise; onDisposeRemoteVideoStreamView?: (userId: string) => Promise; // (undocumented) - onDisposeTogetherModeStreamViews?: () => Promise; + onDisposeTogetherModeStreamView?: () => Promise; onMuteParticipant?: (userId: string) => Promise; onPinParticipant?: (userId: string) => void; onRenderAvatar?: OnRenderAvatarCallback; @@ -5370,6 +5374,8 @@ export interface VideoGalleryProps { showCameraSwitcherInLocalPreview?: boolean; showMuteIndicator?: boolean; spotlightedParticipants?: string[]; + // (undocumented) + startTogetherModeEnabled?: boolean; strings?: Partial; styles?: VideoGalleryStyles; // (undocumented) @@ -5400,7 +5406,7 @@ export type VideoGallerySelector = (state: CallClientState, props: CallingBaseSe spotlightedParticipants?: string[]; maxParticipantsToSpotlight?: number; isTogetherModeActive?: boolean; - canStartTogetherMode?: boolean; + startTogetherModeEnabled?: boolean; togetherModeStreamsMap?: TogetherModeStreamsState; togetherModeSeatingCoordinates?: TogetherModeParticipantSeatingState; }; diff --git a/packages/communication-react/src/index.ts b/packages/communication-react/src/index.ts index 8b21ebda07f..63ee8b6f18d 100644 --- a/packages/communication-react/src/index.ts +++ b/packages/communication-react/src/index.ts @@ -319,7 +319,8 @@ export type { VideoGalleryTogetherModeStreams, VideoGalleryTogetherModeParticipantPosition, VideoGalleryTogetherModeSeatingInfo, - TogetherModeStream + TogetherModeStream, + TogetherModeStreamOptions } from '../../react-components/src'; export type { RaiseHandButtonProps, RaiseHandButtonStrings, RaisedHand } from '../../react-components/src'; diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index 39662bcd922..71403ea031f 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -9,18 +9,10 @@ import { ReactionResources, VideoGalleryTogetherModeParticipantPosition, VideoGalleryLocalParticipant, - VideoGalleryRemoteParticipant, - VideoGalleryTogetherModeSeatingInfo + VideoGalleryRemoteParticipant } from '../types'; /* @conditional-compile-remove(together-mode) */ -import { - getReactionStyleBucket, - IReactionStyleBucket, - ITogetherModeReactionStyleBucket, - moveAnimationStyles, - // opacityAnimationStyles, - spriteAnimationStyles -} from './styles/ReactionOverlay.style'; +import { spriteAnimationStyles } from './styles/ReactionOverlay.style'; /* @conditional-compile-remove(together-mode) */ import { // getCombinedKey, @@ -28,41 +20,39 @@ import { REACTION_START_DISPLAY_SIZE } from './VideoGallery/utils/reactionUtils'; /* @conditional-compile-remove(together-mode) */ -import { mergeStyles, Stack } from '@fluentui/react'; -/* @conditional-compile-remove(together-mode) */ -import { videoContainerStyles } from './styles/VideoTile.styles'; +import { Icon, mergeStyles, Stack, Text } from '@fluentui/react'; /* @conditional-compile-remove(together-mode) */ import { getEmojiResource } from './VideoGallery/utils/videoGalleryLayoutUtils'; /* @conditional-compile-remove(together-mode) */ import { useLocale } from '../localization'; /* @conditional-compile-remove(together-mode) */ import { _HighContrastAwareIcon } from './HighContrastAwareIcon'; +import { + getTogetherModeParticipantOverlayStyle, + getTogetherModeSeatPositionStyle, + ITogetherModeSeatPositionStyle +} from './styles/TogetherMode.styles'; +import { iconContainerStyle } from './styles/VideoTile.styles'; /* @conditional-compile-remove(together-mode) */ /** - * Reaction overlay component props + * Signaling action overlay component props * * Can be used with {@link VideoTile}. * * @internal */ -type VisibleTogetherModeReaction = { +type VisibleTogetherModeSignalingAction = { reaction?: Reaction; isHandRaised?: boolean; isSpotlighted?: boolean; isMuted?: boolean; id?: string; - styleBucket?: ITogetherModeReactionStyleBucket; + seatPositionStyle: ITogetherModeSeatPositionStyle; displayName?: string; showDisplayName?: boolean; }; -/* @conditional-compile-remove(together-mode) */ -// type ReceivedReaction = { -// id: string; -// status: 'animating' | 'completedAnimating' | 'ignored'; -// }; - /* @conditional-compile-remove(together-mode) */ /** * TogetherModeOverlay component renders an empty JSX element. @@ -79,208 +69,114 @@ export const TogetherModeOverlay = React.memo( }) => { const locale = useLocale(); const { reactionResources, remoteParticipants, localParticipant, participantsSeatingArrangement } = props; - // Reactions that are currently being animated - // const [visibleReactions, setVisibleReactions] = useState([]); - const [visibleSignals, setVisibleSignals] = useState>({}); + const [visibleSignals, setVisibleSignals] = useState>({}); - // Dictionary of userId to a reaction status. This is used to track the latest received reaction - // per user to avoid animating the same reaction multiple times and to limit the number of - // active reactions of a certain type. - // const latestReceivedReaction = useRef>({}); + useMemo(() => { + const updatedParticipantsIds = remoteParticipants?.map((participant) => participant.userId) ?? []; + updatedParticipantsIds.push(localParticipant?.userId ?? ''); - const participantsSignals: Record = useMemo(() => { - const signals: Record = {}; - remoteParticipants?.map((particpant) => { - const participantID = particpant.userId; - const togetherModeSeatStyle: ITogetherModeReactionStyleBucket = { - sizeScale: 0.9, - opacityMax: 0.9, - width: participantsSeatingArrangement?.[participantID]?.width ?? 0, - height: participantsSeatingArrangement?.[participantID]?.height ?? 0, - left: participantsSeatingArrangement?.[participantID]?.left ?? 0, - top: participantsSeatingArrangement?.[participantID]?.top ?? 0 - }; - signals[participantID] = { - id: participantID, - reaction: particpant.reaction, - isHandRaised: !!particpant.raisedHand, - isSpotlighted: !!particpant.spotlight, - isMuted: particpant.isMuted, - displayName: !particpant?.displayName - ? locale.strings.videoGallery.displayNamePlaceholder - : particpant?.displayName, - styleBucket: togetherModeSeatStyle - }; + const removedVisibleParticipants = Object.keys(visibleSignals).filter( + (participantId) => !updatedParticipantsIds.includes(participantId) + ); + + removedVisibleParticipants.forEach((participantId) => { + delete visibleSignals[participantId]; }); - if (localParticipant) { - const togetherModeSeatStyle: ITogetherModeReactionStyleBucket = { - sizeScale: 0.9, - opacityMax: 0.9, - width: participantsSeatingArrangement?.[localParticipant.userId]?.width ?? 0, - height: participantsSeatingArrangement?.[localParticipant.userId]?.height ?? 0, - left: participantsSeatingArrangement?.[localParticipant.userId]?.left ?? 0, - top: participantsSeatingArrangement?.[localParticipant.userId]?.top ?? 0 - }; - signals[localParticipant.userId] = { - id: localParticipant.userId, - reaction: localParticipant.reaction, - isHandRaised: !!localParticipant.raisedHand, - isSpotlighted: !!localParticipant.spotlight, - isMuted: localParticipant.isMuted, - displayName: !localParticipant?.displayName - ? locale.strings.videoGallery.displayNamePlaceholder - : localParticipant?.displayName, - styleBucket: togetherModeSeatStyle - }; - } - return signals; - }, [ - remoteParticipants, - localParticipant, - locale.strings.videoGallery.displayNamePlaceholder, - participantsSeatingArrangement - ]); + }, [remoteParticipants, localParticipant, visibleSignals]); const updateTogetherModeSeatingUI = useCallback( - ( - participant: VideoGalleryLocalParticipant | VideoGalleryRemoteParticipant, - seatingPosition: VideoGalleryTogetherModeSeatingInfo - ): void => { - // const combinedKey = getCombinedKey(userId, reaction.reactionType, reaction.receivedOn); - - // const alreadyHandled = latestReceivedReaction.current[userId]?.id === combinedKey; - // if (alreadyHandled) { - // return; - // } - + (participant: VideoGalleryLocalParticipant | VideoGalleryRemoteParticipant): void => { const participantID = participant.userId; - const togetherModeSeatStyle: ITogetherModeReactionStyleBucket = { - sizeScale: 0.9, - opacityMax: 0.9, - width: seatingPosition.width, - height: seatingPosition.height, - left: seatingPosition.left, - top: seatingPosition.top - }; + const seatingPosition = participantsSeatingArrangement?.[participantID]; + if (!seatingPosition) { + return; + } + const togetherModeSeatStyle: ITogetherModeSeatPositionStyle = getTogetherModeSeatPositionStyle(seatingPosition); - // removeVisibleSignalinDiv(participantID); setVisibleSignals((prevVisibleSignals) => ({ ...prevVisibleSignals, - [participant.userId]: { + [participantID]: { id: participantID, reaction: participant.reaction, isHandRaised: !!participant.raisedHand, isSpotlighted: !!participant.spotlight, isMuted: participant.isMuted, displayName: participant.displayName || locale.strings.videoGallery.displayNamePlaceholder, - showDisplayName: !!( - participant.isMuted || - participant.spotlight || - participant.raisedHand || - participant.reaction - ), - styleBucket: togetherModeSeatStyle + showDisplayName: !!(participant.spotlight || participant.raisedHand || participant.reaction), + seatPositionStyle: togetherModeSeatStyle } })); - return; }, - [locale.strings.videoGallery.displayNamePlaceholder] + [locale.strings.videoGallery.displayNamePlaceholder, participantsSeatingArrangement] ); - // const removeVisibleSignalinDiv = (id: string): void => { - // document.getElementById(id)?.remove(); - // }; - - // Update visible reactions when remote participants send a reaction useEffect(() => { - remoteParticipants?.map((participant) => { + remoteParticipants?.forEach((participant) => { if (participant.videoStream?.isAvailable) { - const seatingPosition = participantsSeatingArrangement?.[participant.userId]; - seatingPosition && updateTogetherModeSeatingUI(participant, seatingPosition); + updateTogetherModeSeatingUI(participant); } }); - if (localParticipant && localParticipant.videoStream?.isAvailable) { - const seatingPosition = participantsSeatingArrangement?.[localParticipant.userId]; - seatingPosition && updateTogetherModeSeatingUI(localParticipant, seatingPosition); + + if (localParticipant) { + if (localParticipant.videoStream?.isAvailable) { + updateTogetherModeSeatingUI(localParticipant); + } } - }, [ - remoteParticipants, - localParticipant, - participantsSeatingArrangement, - updateTogetherModeSeatingUI, - participantsSignals - ]); + }, [remoteParticipants, localParticipant, participantsSeatingArrangement, updateTogetherModeSeatingUI]); - const styleBucket = (): IReactionStyleBucket => getReactionStyleBucket(); - const displaySizePx = (): number => REACTION_START_DISPLAY_SIZE * styleBucket().sizeScale; return ( - + {Object.values(visibleSignals).map((participantSignal) => (
{ - // First div - Section that fixes the travel height and applies the movement animation - // Second div - Keeps track of active sprites and responsible for marking, counting - // and removing reactions. Responsible for opacity controls as the sprite emoji animates - // Third div - Responsible for calculating the point of X axis where the reaction will start animation - // Fourth div - Play Animation as the other animation applies on the base play animation for the sprite -
+
-
-
-
-
+ />
} -
-
-
+
+ + {participantSignal.isHandRaised && ( - <_HighContrastAwareIcon disabled={true} iconName="ControlButtonRaiseHand" /> + + + + )} + {participantSignal.showDisplayName && ( + + {participantSignal.displayName} + + )} + {participantSignal.showDisplayName && participantSignal.isMuted && ( + + + )} - {participantSignal.showDisplayName && participantSignal.displayName} - {participantSignal.isMuted && <_HighContrastAwareIcon disabled={true} iconName={'Muted'} />} {participantSignal.isSpotlighted && ( - <_HighContrastAwareIcon disabled={true} iconName={'ControlButtonExitSpotlight'} /> + + + )} -
-
+ +
diff --git a/packages/react-components/src/components/VideoGallery.tsx b/packages/react-components/src/components/VideoGallery.tsx index c35658f8de2..f65dd560b00 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -46,8 +46,6 @@ import { LargeGalleryLayout } from './VideoGallery/LargeGalleryLayout'; /* @conditional-compile-remove(together-mode) */ import { TogetherModeLayout } from './VideoGallery/TogetherModeLayout'; -/* @conditional-compile-remove(together-mode) */ -import { TogetherModeLayoutProps } from './VideoGallery/Layout'; import { LayoutProps } from './VideoGallery/Layout'; import { ReactionResources } from '../types/ReactionTypes'; /* @conditional-compile-remove(together-mode) */ @@ -328,7 +326,7 @@ export interface VideoGalleryProps { */ onMuteParticipant?: (userId: string) => Promise; /* @conditional-compile-remove(together-mode) */ - canStartTogetherMode?: boolean; + startTogetherModeEnabled?: boolean; /* @conditional-compile-remove(together-mode) */ isTogetherModeActive?: boolean; /* @conditional-compile-remove(together-mode) */ @@ -343,7 +341,7 @@ export interface VideoGalleryProps { /* @conditional-compile-remove(together-mode) */ togetherModeSeatingCoordinates?: VideoGalleryTogetherModeParticipantPosition; /* @conditional-compile-remove(together-mode) */ - onDisposeTogetherModeStreamViews?: () => Promise; + onDisposeTogetherModeStreamView?: () => Promise; } /** @@ -430,7 +428,7 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { videoTilesOptions, onMuteParticipant, /* @conditional-compile-remove(together-mode) */ - canStartTogetherMode, + startTogetherModeEnabled, /* @conditional-compile-remove(together-mode) */ isTogetherModeActive, /* @conditional-compile-remove(together-mode) */ @@ -444,7 +442,7 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { /* @conditional-compile-remove(together-mode) */ togetherModeSeatingCoordinates, /* @conditional-compile-remove(together-mode) */ - onDisposeTogetherModeStreamViews + onDisposeTogetherModeStreamView } = props; const ids = useIdentifiers(); @@ -755,6 +753,43 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { ? localScreenShareStreamComponent : undefined; + /* @conditional-compile-remove(together-mode) */ + const togetherModeStreamComponent = useMemo( + () => ( + + ), + [ + startTogetherModeEnabled, + isTogetherModeActive, + onCreateTogetherModeStreamView, + onStartTogetherMode, + onSetTogetherModeSceneSize, + togetherModeStreams, + togetherModeSeatingCoordinates, + onDisposeTogetherModeStreamView, + localParticipant, + remoteParticipants, + reactionResources, + screenShareComponent, + containerWidth, + containerHeight + ] + ); const layoutProps = useMemo( () => ({ remoteParticipants, @@ -771,7 +806,9 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { pinnedParticipantUserIds: pinnedParticipants, overflowGalleryPosition, localVideoTileSize, - spotlightedParticipantUserIds: spotlightedParticipants + spotlightedParticipantUserIds: spotlightedParticipants, + /* @conditional-compile-remove(together-mode) */ + togetherModeStreamComponent }), [ remoteParticipants, @@ -789,53 +826,12 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { pinnedParticipants, overflowGalleryPosition, localVideoTileSize, - spotlightedParticipants - ] - ); - - /* @conditional-compile-remove(together-mode) */ - const togetherModeStreamComponent = useMemo( - () => ( - - ), - [ - canStartTogetherMode, - isTogetherModeActive, - onCreateTogetherModeStreamView, - onStartTogetherMode, - onSetTogetherModeSceneSize, - togetherModeStreams, - togetherModeSeatingCoordinates, - onDisposeTogetherModeStreamViews, - localParticipant, - remoteParticipants, - reactionResources, - containerWidth, - containerHeight + spotlightedParticipants, + /* @conditional-compile-remove(together-mode) */ + togetherModeStreamComponent ] ); - /* @conditional-compile-remove(together-mode) */ - const togetherModeLayoutProps = useMemo(() => { - return { - togetherModeStreamComponent - }; - }, [togetherModeStreamComponent]); - const videoGalleryLayout = useMemo(() => { if (screenShareParticipant && layout === 'focusedContent') { return ; @@ -853,15 +849,10 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { } /* @conditional-compile-remove(together-mode) */ if (layout === 'togetherMode') { - return ; + return ; } return ; - }, [ - layout, - layoutProps, - /* @conditional-compile-remove(together-mode) */ togetherModeLayoutProps, - screenShareParticipant - ]); + }, [layout, layoutProps, screenShareParticipant]); return (
{ - return <>{props.togetherModeStreamComponent}; +export const TogetherModeLayout = (props: LayoutProps): JSX.Element => { + const { + remoteParticipants = [], + dominantSpeakers, + screenShareComponent, + onRenderRemoteParticipant, + styles, + maxRemoteVideoStreams, + parentWidth, + parentHeight, + overflowGalleryPosition = 'horizontalBottom', + pinnedParticipantUserIds = [], + togetherModeStreamComponent + } = props; + const isNarrow = parentWidth ? isNarrowWidth(parentWidth) : false; + + const isShort = parentHeight ? isShortHeight(parentHeight) : false; + + const [indexesToRender, setIndexesToRender] = useState([]); + const childrenPerPage = useRef(4); + + const { gridParticipants, overflowGalleryParticipants } = useOrganizedParticipants({ + remoteParticipants, + dominantSpeakers, + maxGridParticipants: maxRemoteVideoStreams, + isScreenShareActive: !!screenShareComponent, + maxOverflowGalleryDominantSpeakers: screenShareComponent + ? childrenPerPage.current - (pinnedParticipantUserIds.length % childrenPerPage.current) + : childrenPerPage.current, + pinnedParticipantUserIds, + layout: 'floatingLocalVideo' + }); + const { gridTiles, overflowGalleryTiles } = renderTiles( + gridParticipants, + onRenderRemoteParticipant, + maxRemoteVideoStreams, + indexesToRender, + overflowGalleryParticipants, + dominantSpeakers + ); + + const layerHostId = useId('layerhost'); + const togetherModeOverFlowGalleryTiles = useMemo(() => { + let newTiles = overflowGalleryTiles; + if (togetherModeStreamComponent) { + if (screenShareComponent) { + newTiles = gridTiles.concat(overflowGalleryTiles); + } + } + return newTiles; + }, [gridTiles, overflowGalleryTiles, screenShareComponent, togetherModeStreamComponent]); + + const overflowGallery = useMemo(() => { + if (overflowGalleryTiles.length === 0 && !props.screenShareComponent) { + return null; + } + return ( + { + childrenPerPage.current = n; + }} + parentWidth={parentWidth} + /> + ); + }, [ + overflowGalleryTiles.length, + props.screenShareComponent, + isShort, + isNarrow, + togetherModeOverFlowGalleryTiles, + styles?.horizontalGallery, + styles?.verticalGallery, + overflowGalleryPosition, + parentWidth + ]); + + return screenShareComponent ? ( + + + + {props.overflowGalleryPosition === 'horizontalTop' ? overflowGallery : <>} + {screenShareComponent} + {overflowGalleryTrampoline(overflowGallery, props.overflowGalleryPosition)} + + + ) : ( + {props.togetherModeStreamComponent} + ); +}; + +const overflowGalleryTrampoline = ( + gallery: JSX.Element | null, + galleryPosition?: 'horizontalBottom' | 'verticalRight' | 'horizontalTop' +): JSX.Element | null => { + return galleryPosition !== 'horizontalTop' ? gallery : <>; + return gallery; }; diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx index 2b324b79835..045c386251c 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx @@ -19,8 +19,8 @@ import { import { StreamMedia } from '../StreamMedia'; /* @conditional-compile-remove(together-mode) */ import { MeetingReactionOverlay } from '../MeetingReactionOverlay'; -/* @conditional-compile-remove(together-mode) */ -import { Stack } from '@fluentui/react'; +import { mergeStyles, Stack } from '@fluentui/react'; +import { togetherModeRootStyle } from '../styles/TogetherMode.styles'; // Ensure this is an object, not a string /* @conditional-compile-remove(together-mode) */ /** @@ -30,36 +30,33 @@ import { Stack } from '@fluentui/react'; */ export const TogetherModeStream = React.memo( (props: { - canStartTogetherMode?: boolean; + startTogetherModeEnabled?: boolean; isTogetherModeActive?: boolean; onCreateTogetherModeStreamView?: (options?: VideoStreamOptions) => Promise; onStartTogetherMode?: (options?: VideoStreamOptions) => Promise; - onDisposeTogetherModeStreamViews?: () => Promise; + onDisposeTogetherModeStreamView?: () => Promise; onSetTogetherModeSceneSize?: (width: number, height: number) => void; togetherModeStreams?: VideoGalleryTogetherModeStreams; seatingCoordinates?: VideoGalleryTogetherModeParticipantPosition; reactionResources?: ReactionResources; localParticipant?: VideoGalleryLocalParticipant; remoteParticipants?: VideoGalleryRemoteParticipant[]; + screenShareComponent?: JSX.Element; containerWidth?: number; containerHeight?: number; }): React.ReactNode => { const { - canStartTogetherMode, + startTogetherModeEnabled, isTogetherModeActive, onCreateTogetherModeStreamView, onStartTogetherMode, onSetTogetherModeSceneSize, togetherModeStreams, - seatingCoordinates, - reactionResources, - localParticipant, - remoteParticipants, containerWidth, containerHeight } = props; - if (canStartTogetherMode && !isTogetherModeActive) { + if (startTogetherModeEnabled && !isTogetherModeActive) { onStartTogetherMode && onStartTogetherMode(); } @@ -74,39 +71,43 @@ export const TogetherModeStream = React.memo( onSetTogetherModeSceneSize(containerWidth, containerHeight); }, [onSetTogetherModeSceneSize, containerWidth, containerHeight]); - const stream = togetherModeStreams?.mainVideoStream; - const showLoadingIndicator = stream && stream.isAvailable && stream.isReceiving; - - return ( - <> - {containerWidth && containerHeight && ( - -
- - {reactionResources && ( - - )} -
-
- )} - - ); + const layout = getTogetherModeMainVideoLayout(props); + return layout; } ); + +/* @conditional-compile-remove(together-mode) */ +const getTogetherModeMainVideoLayout = (props: { + togetherModeStreams?: VideoGalleryTogetherModeStreams; + containerWidth?: number; + containerHeight?: number; + reactionResources?: ReactionResources; + localParticipant?: VideoGalleryLocalParticipant; + remoteParticipants?: VideoGalleryRemoteParticipant[]; + seatingCoordinates?: VideoGalleryTogetherModeParticipantPosition; +}): JSX.Element | null => { + const stream = props.togetherModeStreams?.mainVideoStream; + const showLoadingIndicator = stream && stream.isAvailable && stream.isReceiving; + + return props.containerWidth && props.containerHeight ? ( + +
+ + {props.reactionResources && ( + + )} +
+
+ ) : null; +}; diff --git a/packages/react-components/src/components/VideoGallery/utils/videoGalleryLayoutUtils.ts b/packages/react-components/src/components/VideoGallery/utils/videoGalleryLayoutUtils.ts index 2fdfb2368a7..43807270c05 100644 --- a/packages/react-components/src/components/VideoGallery/utils/videoGalleryLayoutUtils.ts +++ b/packages/react-components/src/components/VideoGallery/utils/videoGalleryLayoutUtils.ts @@ -176,7 +176,8 @@ export const renderTiles = ( maxRemoteVideoStreams: number, indexesToRender: number[], overflowGalleryParticipants: VideoGalleryParticipant[], - dominantSpeakers?: string[] + dominantSpeakers?: string[], + togetherModeComponent?: JSX.Element ): { gridTiles: JSX.Element[]; overflowGalleryTiles: JSX.Element[] } => { const _dominantSpeakers = dominantSpeakers ?? []; let streamsLeftToRender = maxRemoteVideoStreams; @@ -212,7 +213,7 @@ export const renderTiles = ( (p.videoStream?.isAvailable && streamsLeftToRender-- > 0) ); }); - + togetherModeComponent && overflowGalleryTiles.push(togetherModeComponent); return { gridTiles, overflowGalleryTiles }; }; diff --git a/packages/react-components/src/components/styles/ReactionOverlay.style.ts b/packages/react-components/src/components/styles/ReactionOverlay.style.ts index 36853f96b9b..82c712e529d 100644 --- a/packages/react-components/src/components/styles/ReactionOverlay.style.ts +++ b/packages/react-components/src/components/styles/ReactionOverlay.style.ts @@ -136,19 +136,6 @@ export interface IReactionStyleBucket { heightMinScale?: number; } -/** - * @private - * Interface for defining the style bucket for reactions in Together Mode. - */ -export interface ITogetherModeReactionStyleBucket { - sizeScale: number; - opacityMax: number; - height: number; - width?: number; - left?: number; - top?: number; -} - /** * Return a style bucket based on the number of active sprites. * For example, the first three reactions should appear at maximum diff --git a/packages/react-components/src/components/styles/TogetherMode.styles.ts b/packages/react-components/src/components/styles/TogetherMode.styles.ts new file mode 100644 index 00000000000..1a4a2cc375d --- /dev/null +++ b/packages/react-components/src/components/styles/TogetherMode.styles.ts @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import { _pxToRem } from '@internal/acs-ui-common'; +// import { BaseCustomStyles } from '../../types/CustomStylesProps'; +import { BaseCustomStyles } from '../../types/CustomStylesProps'; +import { VideoGalleryTogetherModeSeatingInfo } from '../../types/TogetherModeTypes'; + +/** + * Interface for defining the coordinates of a seat in Together Mode. + */ +export interface ITogetherModeSeatCoordinates { + height?: number; + width?: number; + left?: number; + top?: number; +} + +/** + * Interface for defining the style of a seat position in Together Mode. + */ +export interface ITogetherModeSeatPositionStyle { + sizeScale: number; + opacityMax: number; + seatCoordinates: ITogetherModeSeatCoordinates; + position?: string; +} + +/** + * Style for the signaling action container. + * This container is used to participant's displayName, + * raisehands, spotlight, and mute icons + */ +export const signalingActionContainerStyle = { + color: 'white', + textAlign: 'center' as const, + backgroundColor: 'black', + display: 'inline-block', + position: 'absolute' as const, + bottom: '0px', + margin: `auto`, + border: '1px solid blue' +}; + +/** + * Generates the root style for Together Mode. + * + * @param width - The width of the root element. + * @param height - The height of the root element. + * @returns The base custom styles for the root element. + */ +export const togetherModeRootStyle = (width: number, height: number): BaseCustomStyles => { + /** + * The root style for Together Mode. + */ + const rootStyle = { + root: { + width: `${width}px`, + height: `${height}px`, + position: 'relative' as const, + top: 0, + left: 0 + } + }; + return rootStyle; +}; + +/** + * Sets the seating position for a participant in Together Mode. + * + * @param seatingPosition - The seating position information. + * @returns The style object for the seating position. + */ +export function setParticipantSeatingPosition( + seatingPosition: VideoGalleryTogetherModeSeatingInfo +): ITogetherModeSeatCoordinates { + return { + width: seatingPosition.width || 0, + height: seatingPosition.height || 0, + left: seatingPosition.left || 0, + top: seatingPosition.top || 0 + }; +} +/** + * Return a style bucket based on the number of active sprites. + * For example, the first three reactions should appear at maximum + * height, width, and opacity. + * @private + */ +export function getTogetherModeSeatPositionStyle( + seatingPosition: VideoGalleryTogetherModeSeatingInfo +): ITogetherModeSeatPositionStyle { + return { + sizeScale: 0.9, + opacityMax: 0.9, + seatCoordinates: setParticipantSeatingPosition(seatingPosition) + }; +} + +/** + * Generates the overlay style for a participant in Together Mode. + * + * @param seatingPosition - The seating position information. + * @returns The style object for the participant overlay. + */ +export function getTogetherModeParticipantOverlayStyle( + seatingPositionStyle: ITogetherModeSeatPositionStyle +): React.CSSProperties { + return { + position: 'absolute', + border: '1px solid green', + ...seatingPositionStyle.seatCoordinates + }; +} diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index 1140de57507..f9b950eda3b 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -70,7 +70,8 @@ export type { TogetherModeStream, VideoGalleryTogetherModeParticipantPosition, VideoGalleryTogetherModeSeatingInfo, - VideoGalleryTogetherModeStreams + VideoGalleryTogetherModeStreams, + TogetherModeStreamOptions } from './types'; export type { RaisedHand } from './types'; diff --git a/packages/react-components/src/types/TogetherModeTypes.ts b/packages/react-components/src/types/TogetherModeTypes.ts index 3a2aef91377..1f916acb2d2 100644 --- a/packages/react-components/src/types/TogetherModeTypes.ts +++ b/packages/react-components/src/types/TogetherModeTypes.ts @@ -2,8 +2,16 @@ // Licensed under the MIT License. /* @conditional-compile-remove(together-mode) */ -import { CreateVideoStreamViewResult, ViewScalingMode } from './VideoGalleryParticipant'; +import { CreateVideoStreamViewResult, VideoStreamOptions, ViewScalingMode } from './VideoGalleryParticipant'; +/* @conditional-compile-remove(together-mode) */ +/** + * Interface representing the result of a Together Mode stream view. + * @beta + */ +export interface TogetherModeStreamOptions extends VideoStreamOptions { + viewKind?: 'main' | 'panoramic'; +} /* @conditional-compile-remove(together-mode) */ /** * Interface representing the result of a Together Mode stream view. diff --git a/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts b/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts index c0b3220c9dd..3f038738afb 100644 --- a/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts @@ -111,8 +111,8 @@ export class _MockCallAdapter implements CallAdapter { throw Error('createStreamView not implemented'); } /* @conditional-compile-remove(together-mode) */ - createTogetherModeStreamViews(): Promise { - throw Error('createTogetherModeStreamViews not implemented'); + createTogetherModeStreamView(): Promise { + throw Error('createTogetherModeStreamView not implemented'); } /* @conditional-compile-remove(together-mode) */ startTogetherMode(): Promise { @@ -135,7 +135,7 @@ export class _MockCallAdapter implements CallAdapter { return Promise.resolve(); } /* @conditional-compile-remove(together-mode) */ - disposeTogetherModeStreamViews(): Promise { + disposeTogetherModeStreamView(): Promise { return Promise.resolve(); } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts b/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts index c7c78a8c438..951d066ca5b 100644 --- a/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts @@ -117,7 +117,7 @@ import { CallSurvey, CallSurveyResponse } from '@azure/communication-calling'; import { CallingSoundSubscriber } from './CallingSoundSubscriber'; import { CallingSounds } from './CallAdapter'; /* @conditional-compile-remove(together-mode) */ -import { TogetherModeStreamViewResult } from '@internal/react-components'; +import { TogetherModeStreamViewResult, TogetherModeStreamOptions } from '@internal/react-components'; type CallTypeOf = AgentType extends CallAgent ? Call : TeamsCall; @@ -600,13 +600,13 @@ export class AzureCommunicationCallAdapter { return await this.handlers.onCreateTogetherModeStreamView(options); } @@ -822,8 +822,8 @@ export class AzureCommunicationCallAdapter { - return await this.handlers.onDisposeTogetherModeStreamViews(); + public async disposeTogetherModeStreamView(): Promise { + return await this.handlers.onDisposeTogetherModeStreamView(); } public async leaveCall(forEveryone?: boolean): Promise { diff --git a/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts b/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts index 410a7de29af..fea02eb38dc 100644 --- a/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/adapter/CallAdapter.ts @@ -40,7 +40,7 @@ import { import { CallSurvey, CallSurveyResponse } from '@azure/communication-calling'; import { ReactionResources } from '@internal/react-components'; /* @conditional-compile-remove(together-mode) */ -import { TogetherModeStreamViewResult } from '@internal/react-components'; +import { TogetherModeStreamViewResult, TogetherModeStreamOptions } from '@internal/react-components'; /** * Major UI screens shown in the {@link CallComposite}. * @@ -622,7 +622,7 @@ export interface CallAdapterCallOperations { * * @beta */ - createTogetherModeStreamViews(options?: VideoStreamOptions): Promise; + createTogetherModeStreamView(options?: TogetherModeStreamOptions): Promise; /* @conditional-compile-remove(together-mode) */ /** * Start Together mode. @@ -657,7 +657,7 @@ export interface CallAdapterCallOperations { * * @beta */ - disposeTogetherModeStreamViews(): Promise; + disposeTogetherModeStreamView(): Promise; /** * Dispose the html view for a screen share stream * diff --git a/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts b/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts index 4ce0a1ccb5a..82e92f5ddd4 100644 --- a/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts +++ b/packages/react-composites/src/composites/CallComposite/hooks/useHandlers.ts @@ -231,7 +231,7 @@ const createCompositeHandlers = memoizeOne( }, /* @conditional-compile-remove(together-mode) */ onCreateTogetherModeStreamView: async (options) => { - return await adapter.createTogetherModeStreamViews(options); + return await adapter.createTogetherModeStreamView(options); }, /* @conditional-compile-remove(together-mode) */ onStartTogetherMode: async () => { @@ -242,8 +242,8 @@ const createCompositeHandlers = memoizeOne( return adapter.setTogetherModeSceneSize(width, height); }, /* @conditional-compile-remove(together-mode) */ - onDisposeTogetherModeStreamViews: async () => { - return await adapter.disposeTogetherModeStreamViews(); + onDisposeTogetherModeStreamView: async () => { + return await adapter.disposeTogetherModeStreamView(); } }; } diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts index cbf7bdfd592..166909d9490 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts @@ -27,7 +27,7 @@ import { MessageOptions } from '@internal/acs-ui-common'; /* @conditional-compile-remove(breakout-rooms) */ import { toFlatCommunicationIdentifier } from '@internal/acs-ui-common'; /* @conditional-compile-remove(together-mode) */ -import { TogetherModeStreamViewResult } from '@internal/react-components'; +import { TogetherModeStreamViewResult, TogetherModeStreamOptions } from '@internal/react-components'; import { ParticipantsJoinedListener, ParticipantsLeftListener, @@ -521,10 +521,10 @@ export class AzureCommunicationCallWithChatAdapter implements CallWithChatAdapte await this.callAdapter.disposeLocalVideoStreamView(); } /* @conditional-compile-remove(together-mode) */ - public async createTogetherModeStreamViews( - options?: VideoStreamOptions + public async createTogetherModeStreamView( + options?: TogetherModeStreamOptions ): Promise { - return await this.callAdapter.createTogetherModeStreamViews(options); + return await this.callAdapter.createTogetherModeStreamView(options); } /* @conditional-compile-remove(together-mode) */ public async startTogetherMode(): Promise { @@ -535,8 +535,8 @@ export class AzureCommunicationCallWithChatAdapter implements CallWithChatAdapte return this.callAdapter.setTogetherModeSceneSize(width, height); } /* @conditional-compile-remove(together-mode) */ - public async disposeTogetherModeStreamViews(): Promise { - await this.callAdapter.disposeTogetherModeStreamViews(); + public async disposeTogetherModeStreamView(): Promise { + await this.callAdapter.disposeTogetherModeStreamView(); } /** Fetch initial Call and Chat data such as chat messages. */ public async fetchInitialData(): Promise { diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts index 54dd12f7564..1fb15ea825a 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatAdapter.ts @@ -42,7 +42,7 @@ import { BreakoutRoomsUpdatedListener } from '@azure/communication-calling'; import { DtmfTone } from '@azure/communication-calling'; import { CreateVideoStreamViewResult, VideoStreamOptions } from '@internal/react-components'; /* @conditional-compile-remove(together-mode) */ -import { TogetherModeStreamViewResult } from '@internal/react-components'; +import { TogetherModeStreamViewResult, TogetherModeStreamOptions } from '@internal/react-components'; import { SendMessageOptions } from '@azure/communication-chat'; /* @conditional-compile-remove(rich-text-editor-image-upload) */ import { UploadChatImageResult } from '@internal/acs-ui-common'; @@ -239,7 +239,7 @@ export interface CallWithChatAdapterManagement { * * @beta */ - createTogetherModeStreamViews(options?: VideoStreamOptions): Promise; + createTogetherModeStreamView(options?: TogetherModeStreamOptions): Promise; /* @conditional-compile-remove(together-mode) */ /** * Start together mode. @@ -274,7 +274,7 @@ export interface CallWithChatAdapterManagement { * * @beta */ - disposeTogetherModeStreamViews(): Promise; + disposeTogetherModeStreamView(): Promise; /** * Dispose the html view for a screen share stream * diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts index fa078b0b6a4..db1b828a4da 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts @@ -7,7 +7,7 @@ import { CallAdapter, CallAdapterState } from '../../CallComposite'; import { VideoBackgroundImage, VideoBackgroundEffect } from '../../CallComposite'; import { CreateVideoStreamViewResult, VideoStreamOptions } from '@internal/react-components'; /* @conditional-compile-remove(together-mode) */ -import { TogetherModeStreamViewResult } from '@internal/react-components'; +import { TogetherModeStreamViewResult, TogetherModeStreamOptions } from '@internal/react-components'; import { AudioDeviceInfo, VideoDeviceInfo, @@ -138,10 +138,10 @@ export class CallWithChatBackedCallAdapter implements CallAdapter { ): Promise => await this.callWithChatAdapter.createStreamView(remoteUserId, options); /* @conditional-compile-remove(together-mode) */ - public createTogetherModeStreamViews = async ( - options?: VideoStreamOptions + public createTogetherModeStreamView = async ( + options?: TogetherModeStreamOptions ): Promise => - await this.callWithChatAdapter.createTogetherModeStreamViews(options); + await this.callWithChatAdapter.createTogetherModeStreamView(options); /* @conditional-compile-remove(together-mode) */ public startTogetherMode = async (): Promise => await this.callWithChatAdapter.startTogetherMode(); /* @conditional-compile-remove(together-mode) */ @@ -159,8 +159,8 @@ export class CallWithChatBackedCallAdapter implements CallAdapter { return this.callWithChatAdapter.disposeLocalVideoStreamView(); } /* @conditional-compile-remove(together-mode) */ - public disposeTogetherModeStreamViews = async (): Promise => - await this.callWithChatAdapter.disposeTogetherModeStreamViews(); + public disposeTogetherModeStreamView = async (): Promise => + await this.callWithChatAdapter.disposeTogetherModeStreamView(); public holdCall = async (): Promise => { await this.callWithChatAdapter.holdCall(); }; diff --git a/packages/react-composites/tests/app/lib/MockCallAdapter.ts b/packages/react-composites/tests/app/lib/MockCallAdapter.ts index 2d02477589b..8138b8b97d0 100644 --- a/packages/react-composites/tests/app/lib/MockCallAdapter.ts +++ b/packages/react-composites/tests/app/lib/MockCallAdapter.ts @@ -92,8 +92,8 @@ export class MockCallAdapter implements CallAdapter { throw Error('disposeStreamView not implemented'); } /* @conditional-compile-remove(together-mode) */ - createTogetherModeStreamViews(): Promise { - throw Error('createTogetherModeStreamViews not implemented'); + createTogetherModeStreamView(): Promise { + throw Error('createTogetherModeStreamView not implemented'); } /* @conditional-compile-remove(together-mode) */ startTogetherMode(): Promise { @@ -104,7 +104,7 @@ export class MockCallAdapter implements CallAdapter { throw Error(`Setting Together Mode width ${width} and height: ${height} not implemented`); } /* @conditional-compile-remove(together-mode) */ - disposeTogetherModeStreamViews(): Promise { + disposeTogetherModeStreamView(): Promise { throw Error('disposeFeatureStreamView not implemented'); } disposeScreenShareStreamView(): Promise { From 0abd7d3d35f2b4bc92c5e7b140a5cd99f67115d9 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Mon, 2 Dec 2024 22:05:06 +0000 Subject: [PATCH 13/37] clean up imports --- .../src/components/VideoGallery/TogetherModeStream.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx index 045c386251c..d74b8fb2ec4 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx @@ -19,7 +19,7 @@ import { import { StreamMedia } from '../StreamMedia'; /* @conditional-compile-remove(together-mode) */ import { MeetingReactionOverlay } from '../MeetingReactionOverlay'; -import { mergeStyles, Stack } from '@fluentui/react'; +import { Stack } from '@fluentui/react'; import { togetherModeRootStyle } from '../styles/TogetherMode.styles'; // Ensure this is an object, not a string /* @conditional-compile-remove(together-mode) */ From 3f1c60160de9adf872af039a9fd7387d52acfcbe Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Wed, 4 Dec 2024 18:34:45 +0000 Subject: [PATCH 14/37] Updated API and cleaned up reactions overlay --- .../review/beta/communication-react.api.md | 14 +---- packages/communication-react/src/index.ts | 1 - .../src/components/MeetingReactionOverlay.tsx | 11 +++- .../src/components/TogetherModeOverlay.tsx | 25 +++++++-- .../VideoGallery/TogetherModeLayout.tsx | 8 +++ .../VideoGallery/TogetherModeStream.tsx | 52 ++++++++++++++----- .../components/styles/TogetherMode.styles.ts | 52 ++++++++++--------- packages/react-components/src/index.ts | 1 - .../src/types/TogetherModeTypes.ts | 34 +----------- 9 files changed, 108 insertions(+), 90 deletions(-) diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index a8bfa2bfc2d..19047361b01 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -5065,18 +5065,6 @@ export interface TogetherModeSeatingPositionState { width: number; } -// @beta -export interface TogetherModeStream { - isAvailable?: boolean; - isReceiving?: boolean; - renderElement?: HTMLElement; - scalingMode?: ViewScalingMode; - streamSize?: { - width: number; - height: number; - }; -} - // @beta export interface TogetherModeStreamOptions extends VideoStreamOptions { // (undocumented) @@ -5487,7 +5475,7 @@ export interface VideoGalleryTogetherModeSeatingInfo { // @beta export interface VideoGalleryTogetherModeStreams { // (undocumented) - mainVideoStream?: TogetherModeStream; + mainVideoStream?: VideoGalleryStream; } // @public diff --git a/packages/communication-react/src/index.ts b/packages/communication-react/src/index.ts index 63ee8b6f18d..24b2f3adc0c 100644 --- a/packages/communication-react/src/index.ts +++ b/packages/communication-react/src/index.ts @@ -319,7 +319,6 @@ export type { VideoGalleryTogetherModeStreams, VideoGalleryTogetherModeParticipantPosition, VideoGalleryTogetherModeSeatingInfo, - TogetherModeStream, TogetherModeStreamOptions } from '../../react-components/src'; diff --git a/packages/react-components/src/components/MeetingReactionOverlay.tsx b/packages/react-components/src/components/MeetingReactionOverlay.tsx index 36ac9430797..2243c4639c5 100644 --- a/packages/react-components/src/components/MeetingReactionOverlay.tsx +++ b/packages/react-components/src/components/MeetingReactionOverlay.tsx @@ -135,7 +135,16 @@ export const MeetingReactionOverlay = (props: MeetingReactionOverlayProps): JSX. } else if (props.overlayMode === 'together-mode') { /* @conditional-compile-remove(together-mode) */ return ( -
+
+
{Object.values(visibleSignals).map((participantSignal) => (
-
+
{
))} - +
); } ); diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx index f732b156ab2..17a4c6ac3ba 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx @@ -3,6 +3,7 @@ /* @conditional-compile-remove(together-mode) */ import React, { useMemo, useRef, useState } from 'react'; +/* @conditional-compile-remove(together-mode) */ import { useId } from '@fluentui/react-hooks'; /* @conditional-compile-remove(together-mode) */ import { _formatString } from '@internal/acs-ui-common'; @@ -10,11 +11,17 @@ import { _formatString } from '@internal/acs-ui-common'; import { LayoutProps } from './Layout'; /* @conditional-compile-remove(together-mode) */ import { LayerHost, mergeStyles, Stack } from '@fluentui/react'; +/* @conditional-compile-remove(together-mode) */ import { renderTiles, useOrganizedParticipants } from './utils/videoGalleryLayoutUtils'; +/* @conditional-compile-remove(together-mode) */ import { OverflowGallery } from './OverflowGallery'; +/* @conditional-compile-remove(together-mode) */ import { rootLayoutStyle } from './styles/DefaultLayout.styles'; +/* @conditional-compile-remove(together-mode) */ import { isNarrowWidth, isShortHeight } from '../utils/responsive'; +/* @conditional-compile-remove(together-mode) */ import { innerLayoutStyle, layerHostStyle } from './styles/FloatingLocalVideoLayout.styles'; +/* @conditional-compile-remove(together-mode) */ import { videoGalleryLayoutGap } from './styles/Layout.styles'; /* @conditional-compile-remove(together-mode) */ @@ -125,6 +132,7 @@ export const TogetherModeLayout = (props: LayoutProps): JSX.Element => { ); }; +/* @conditional-compile-remove(together-mode) */ const overflowGalleryTrampoline = ( gallery: JSX.Element | null, galleryPosition?: 'horizontalBottom' | 'verticalRight' | 'horizontalTop' diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx index d74b8fb2ec4..a1cfa82d77a 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx @@ -2,7 +2,7 @@ // Licensed under the MIT License. /* @conditional-compile-remove(together-mode) */ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; /* @conditional-compile-remove(together-mode) */ import { _formatString, _pxToRem } from '@internal/acs-ui-common'; /* @conditional-compile-remove(together-mode) */ @@ -19,7 +19,9 @@ import { import { StreamMedia } from '../StreamMedia'; /* @conditional-compile-remove(together-mode) */ import { MeetingReactionOverlay } from '../MeetingReactionOverlay'; +/* @conditional-compile-remove(together-mode) */ import { Stack } from '@fluentui/react'; +/* @conditional-compile-remove(together-mode) */ import { togetherModeRootStyle } from '../styles/TogetherMode.styles'; // Ensure this is an object, not a string /* @conditional-compile-remove(together-mode) */ @@ -56,22 +58,39 @@ export const TogetherModeStream = React.memo( containerHeight } = props; - if (startTogetherModeEnabled && !isTogetherModeActive) { - onStartTogetherMode && onStartTogetherMode(); - } + const [sceneDimensions, setSceneDimensions] = useState<{ width: number; height: number }>({ width: 0, height: 0 }); + // Trigger startTogetherMode only when needed + useEffect(() => { + if (startTogetherModeEnabled && !isTogetherModeActive) { + onStartTogetherMode?.(); + } + }, [startTogetherModeEnabled, isTogetherModeActive, onStartTogetherMode]); - if (!togetherModeStreams?.mainVideoStream?.renderElement) { - onCreateTogetherModeStreamView && onCreateTogetherModeStreamView(); - } + // Create stream view if not already created + useEffect(() => { + if (!togetherModeStreams?.mainVideoStream?.renderElement) { + onCreateTogetherModeStreamView?.(); + } + }, [togetherModeStreams?.mainVideoStream?.renderElement, onCreateTogetherModeStreamView]); + // Update scene size only when container dimensions change useEffect(() => { - onSetTogetherModeSceneSize && - containerWidth && - containerHeight && - onSetTogetherModeSceneSize(containerWidth, containerHeight); - }, [onSetTogetherModeSceneSize, containerWidth, containerHeight]); + if (!onSetTogetherModeSceneSize) { + console.log(`Chuk Scene change callback is null ${containerWidth} ${containerHeight}`); + } + if (onSetTogetherModeSceneSize && containerWidth && containerHeight) { + if (containerWidth !== sceneDimensions.width || containerHeight !== sceneDimensions.height) { + onSetTogetherModeSceneSize(containerWidth, containerHeight); + console.log(`Setting scene size width to ${containerWidth} == ${sceneDimensions.width}`); + console.log(`Setting scene size height to ${containerHeight} == ${sceneDimensions.height}`); + setSceneDimensions({ width: containerWidth, height: containerHeight }); + } + } + }, [onSetTogetherModeSceneSize, containerWidth, containerHeight, sceneDimensions.width, sceneDimensions.height]); + // Memoized layout computation const layout = getTogetherModeMainVideoLayout(props); + return layout; } ); @@ -91,7 +110,14 @@ const getTogetherModeMainVideoLayout = (props: { return props.containerWidth && props.containerHeight ? ( -
+
Date: Fri, 6 Dec 2024 00:48:03 +0000 Subject: [PATCH 15/37] Implementation of together mode notification when started or ended --- .../src/notificationStackSelector.ts | 16 ++++++++++++++++ .../src/CallClientState.ts | 4 +++- .../src/TogetherModeSubscriber.ts | 12 ++++++++++++ .../review/beta/communication-react.api.md | 6 +++++- .../src/components/NotificationStack.tsx | 10 ++++++++++ .../src/localization/locales/en-US/strings.json | 8 ++++++++ .../src/composites/CallComposite/Strings.tsx | 12 ++++++++++++ 7 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/calling-component-bindings/src/notificationStackSelector.ts b/packages/calling-component-bindings/src/notificationStackSelector.ts index 75890c5a5bc..aa4177ede44 100644 --- a/packages/calling-component-bindings/src/notificationStackSelector.ts +++ b/packages/calling-component-bindings/src/notificationStackSelector.ts @@ -258,6 +258,22 @@ export const notificationStackSelector: NotificationStackSelector = createSelect timestamp: latestNotifications['breakoutRoomClosingSoon'].timestamp }); } + /* @conditional-compile-remove(together-mode) */ + if (latestNotifications['togetherModeStarted']) { + activeNotifications.push({ + type: 'togetherModeStarted', + timestamp: latestNotifications['togetherModeStarted'].timestamp, + autoDismiss: true + }); + } + /* @conditional-compile-remove(together-mode) */ + if (latestNotifications['togetherModeEnded']) { + activeNotifications.push({ + type: 'togetherModeEnded', + timestamp: latestNotifications['togetherModeEnded'].timestamp, + autoDismiss: true + }); + } return { activeErrorMessages: activeErrorMessages, activeNotifications: activeNotifications }; } ); diff --git a/packages/calling-stateful-client/src/CallClientState.ts b/packages/calling-stateful-client/src/CallClientState.ts index 159e3d6cc8e..406274bef91 100644 --- a/packages/calling-stateful-client/src/CallClientState.ts +++ b/packages/calling-stateful-client/src/CallClientState.ts @@ -1143,7 +1143,9 @@ export type NotificationTarget = | 'assignedBreakoutRoomOpenedPromptJoin' | 'assignedBreakoutRoomChanged' | 'breakoutRoomJoined' - | 'breakoutRoomClosingSoon'; + | 'breakoutRoomClosingSoon' + | 'togetherModeStarted' + | 'togetherModeEnded'; /** * State only proxy for {@link @azure/communication-calling#DiagnosticsCallFeature}. diff --git a/packages/calling-stateful-client/src/TogetherModeSubscriber.ts b/packages/calling-stateful-client/src/TogetherModeSubscriber.ts index 1a387d8be69..d9c0e08d9d0 100644 --- a/packages/calling-stateful-client/src/TogetherModeSubscriber.ts +++ b/packages/calling-stateful-client/src/TogetherModeSubscriber.ts @@ -80,6 +80,7 @@ export class TogetherModeSubscriber { ): void => { for (const stream of removedStreams) { this._togetherModeVideoStreamSubscribers.get(stream.id)?.unsubscribe(); + this._togetherModeVideoStreamSubscribers.delete(stream.id); this._internalContext.deleteCallFeatureRenderInfo( this._callIdRef.callId, this._featureName, @@ -107,6 +108,17 @@ export class TogetherModeSubscriber { convertSdkCallFeatureStreamToDeclarativeCallFeatureStream(stream, this._featureName) ) ); + if (this._togetherMode.togetherModeStream.length) { + this._context.setLatestNotification(this._callIdRef.callId, { + target: 'togetherModeStarted', + timestamp: new Date(Date.now()) + }); + } else { + this._context.setLatestNotification(this._callIdRef.callId, { + target: 'togetherModeEnded', + timestamp: new Date(Date.now()) + }); + } }; private onTogetherModeStreamUpdated = (args: { diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index 32bac6961cb..e34cd1bad7c 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -995,6 +995,8 @@ export interface CallCompositeStrings { tagsSurveyTextFieldDefaultText: string; threeParticipantJoinedNoticeString: string; threeParticipantLeftNoticeString: string; + togetherModeEnded?: string; + togetherModeStarted?: string; transferPageNoticeString: string; transferPageTransferorText: string; transferPageTransferTargetText: string; @@ -4027,6 +4029,8 @@ export interface NotificationStackStrings { stopScreenShareGeneric?: NotificationStrings; stopVideoGeneric?: NotificationStrings; teamsMeetingCallNetworkQualityLow?: NotificationStrings; + togetherModeEnded?: NotificationStrings; + togetherModeStarted?: NotificationStrings; transcriptionStarted?: NotificationStrings; transcriptionStopped?: NotificationStrings; transcriptionStoppedStillRecording?: NotificationStrings; @@ -4056,7 +4060,7 @@ export interface NotificationStyles { } // @public (undocumented) -export type NotificationTarget = 'assignedBreakoutRoomOpened' | 'assignedBreakoutRoomOpenedPromptJoin' | 'assignedBreakoutRoomChanged' | 'breakoutRoomJoined' | 'breakoutRoomClosingSoon'; +export type NotificationTarget = 'assignedBreakoutRoomOpened' | 'assignedBreakoutRoomOpenedPromptJoin' | 'assignedBreakoutRoomChanged' | 'breakoutRoomJoined' | 'breakoutRoomClosingSoon' | 'togetherModeStarted' | 'togetherModeEnded'; // @public export type NotificationType = keyof NotificationStackStrings; diff --git a/packages/react-components/src/components/NotificationStack.tsx b/packages/react-components/src/components/NotificationStack.tsx index c01f5ed2e9c..24d016dce33 100644 --- a/packages/react-components/src/components/NotificationStack.tsx +++ b/packages/react-components/src/components/NotificationStack.tsx @@ -268,6 +268,16 @@ export interface NotificationStackStrings { * Message shown in notification when breakout room is closing soon */ breakoutRoomClosingSoon?: NotificationStrings; + /* @conditional-compile-remove(breakout-rooms) */ + /** + * Speaking while muted message + */ + togetherModeStarted?: NotificationStrings; + /* @conditional-compile-remove(breakout-rooms) */ + /** + * Speaking while muted message + */ + togetherModeEnded?: NotificationStrings; } /** diff --git a/packages/react-components/src/localization/locales/en-US/strings.json b/packages/react-components/src/localization/locales/en-US/strings.json index 7fa8a6fd339..8046d7899fb 100644 --- a/packages/react-components/src/localization/locales/en-US/strings.json +++ b/packages/react-components/src/localization/locales/en-US/strings.json @@ -497,6 +497,14 @@ "title": "Room time limit about to expire.", "message": "This room will close in 30 seconds", "dismissButtonAriaLabel": "Close" + }, + "togetherModeStarted": { + "title": "Togethermode has started", + "dismissButtonAriaLabel": "Close" + }, + "togetherModeEnded": { + "title": "Togethermode has ended", + "dismissButtonAriaLabel": "Close" } }, "videoGallery": { diff --git a/packages/react-composites/src/composites/CallComposite/Strings.tsx b/packages/react-composites/src/composites/CallComposite/Strings.tsx index 6dee8a537e7..0be2ff728f4 100644 --- a/packages/react-composites/src/composites/CallComposite/Strings.tsx +++ b/packages/react-composites/src/composites/CallComposite/Strings.tsx @@ -892,4 +892,16 @@ export interface CallCompositeStrings { * notification. */ returnFromBreakoutRoomBannerButtonLabel: string; + /* @conditional-compile-remove(together-mode) */ + /** + * Label for button in banner to return from breakout room. The banner is shown in mobile view instead of the + * notification. + */ + togetherModeStarted?: string; + /* @conditional-compile-remove(together-mode) */ + /** + * Label for button in banner to return from breakout room. The banner is shown in mobile view instead of the + * notification. + */ + togetherModeEnded?: string; } From 6a3f3264791d25d6970eb1781001803eaafd44a2 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Fri, 6 Dec 2024 06:43:32 +0000 Subject: [PATCH 16/37] Logic to switch back to default view if together mode is no longer active --- .../react-components/src/components/VideoGallery.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/react-components/src/components/VideoGallery.tsx b/packages/react-components/src/components/VideoGallery.tsx index f65dd560b00..dc1ddfc3431 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -30,7 +30,7 @@ import { LocalScreenShare } from './VideoGallery/LocalScreenShare'; import { RemoteScreenShare } from './VideoGallery/RemoteScreenShare'; import { LocalVideoCameraCycleButtonProps } from './LocalVideoCameraButton'; import { _ICoordinates, _ModalClone } from './ModalClone/ModalClone'; -import { _formatString } from '@internal/acs-ui-common'; +import { _formatString, _isIdentityMicrosoftTeamsUser } from '@internal/acs-ui-common'; import { _LocalVideoTile } from './LocalVideoTile'; import { DefaultLayout } from './VideoGallery/DefaultLayout'; import { FloatingLocalVideoLayout } from './VideoGallery/FloatingLocalVideoLayout'; @@ -790,6 +790,10 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { containerHeight ] ); + // Current implementation of capabilities is only based on user role. + // This logic checks for the user role and if the user is a Teams user. + const canStartTogetherMode = _isIdentityMicrosoftTeamsUser(localParticipant.userId) && startTogetherModeEnabled; + const layoutProps = useMemo( () => ({ remoteParticipants, @@ -848,11 +852,13 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { return ; } /* @conditional-compile-remove(together-mode) */ - if (layout === 'togetherMode') { + // Teams users can switch to Together mode layout only if they have the capability, + // while ACS users can do so only if Together mode is enabled. + if (layout === 'togetherMode' && (canStartTogetherMode || isTogetherModeActive)) { return ; } return ; - }, [layout, layoutProps, screenShareParticipant]); + }, [canStartTogetherMode, isTogetherModeActive, layout, layoutProps, screenShareParticipant]); return (
Date: Fri, 6 Dec 2024 07:06:18 +0000 Subject: [PATCH 17/37] Logic to switch back to default view if together mode is no longer active --- .../src/components/VideoGallery.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/react-components/src/components/VideoGallery.tsx b/packages/react-components/src/components/VideoGallery.tsx index f65dd560b00..94904216773 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -30,7 +30,7 @@ import { LocalScreenShare } from './VideoGallery/LocalScreenShare'; import { RemoteScreenShare } from './VideoGallery/RemoteScreenShare'; import { LocalVideoCameraCycleButtonProps } from './LocalVideoCameraButton'; import { _ICoordinates, _ModalClone } from './ModalClone/ModalClone'; -import { _formatString } from '@internal/acs-ui-common'; +import { _formatString, _isIdentityMicrosoftTeamsUser } from '@internal/acs-ui-common'; import { _LocalVideoTile } from './LocalVideoTile'; import { DefaultLayout } from './VideoGallery/DefaultLayout'; import { FloatingLocalVideoLayout } from './VideoGallery/FloatingLocalVideoLayout'; @@ -790,6 +790,11 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { containerHeight ] ); + // Current implementation of capabilities is only based on user role. + // This logic checks for the user role and if the user is a Teams user. + const canSwitchToTogetherModeLayout = + isTogetherModeActive || (_isIdentityMicrosoftTeamsUser(localParticipant.userId) && startTogetherModeEnabled); + const layoutProps = useMemo( () => ({ remoteParticipants, @@ -848,11 +853,13 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { return ; } /* @conditional-compile-remove(together-mode) */ - if (layout === 'togetherMode') { + // Teams users can switch to Together mode layout only if they have the capability, + // while ACS users can do so only if Together mode is enabled. + if (layout === 'togetherMode' && canSwitchToTogetherModeLayout) { return ; } return ; - }, [layout, layoutProps, screenShareParticipant]); + }, [canSwitchToTogetherModeLayout, layout, layoutProps, screenShareParticipant]); return (
Date: Fri, 6 Dec 2024 07:12:54 +0000 Subject: [PATCH 18/37] Merge branch 'cnwankwo/TogetherModeStream_Impl' of https://github.com/Azure/communication-ui-library into cnwankwo/TogetherModeStream_Impl --- ...-050e392e-7a15-4f97-8415-a0b53cc93fda.json | 9 +++ .../playwright/playwright.config.common.ts | 2 +- .../src/captionsSelector.ts | 12 ++-- .../src/hooks/usePropsFor.ts | 10 +++- .../calling-component-bindings/src/index.ts | 4 +- .../review/beta/communication-react.api.md | 32 ++++++++++- .../review/stable/communication-react.api.md | 32 ++++++++++- packages/communication-react/src/index.ts | 6 ++ .../src/components/Caption.tsx | 4 +- .../src/components/CaptionsBanner.tsx | 57 +++++++++++++++---- .../src/components/RealTimeText.tsx | 2 - .../src/localization/LocalizationProvider.tsx | 5 +- .../localization/locales/en-US/strings.json | 3 + .../components/CallArrangement.tsx | 4 +- ...nsBanner.tsx => CallingCaptionsBanner.tsx} | 21 +++---- .../CaptionsBanner/CaptionsBanner.story.tsx | 6 +- .../CaptionsBanner/Docs.mdx | 4 +- .../CaptionsBanner/index.stories.tsx | 12 ++-- .../CaptionsBanner/mockCaptions.ts | 10 ++-- .../snippets/CaptionsBanner.snippet.tsx | 6 +- 20 files changed, 179 insertions(+), 62 deletions(-) create mode 100644 change-beta/@azure-communication-react-050e392e-7a15-4f97-8415-a0b53cc93fda.json rename packages/react-composites/src/composites/common/{CaptionsBanner.tsx => CallingCaptionsBanner.tsx} (85%) rename packages/storybook8/stories/{INTERNAL => Components}/CaptionsBanner/CaptionsBanner.story.tsx (86%) rename packages/storybook8/stories/{INTERNAL => Components}/CaptionsBanner/Docs.mdx (82%) rename packages/storybook8/stories/{INTERNAL => Components}/CaptionsBanner/index.stories.tsx (54%) rename packages/storybook8/stories/{INTERNAL => Components}/CaptionsBanner/mockCaptions.ts (83%) rename packages/storybook8/stories/{INTERNAL => Components}/CaptionsBanner/snippets/CaptionsBanner.snippet.tsx (86%) diff --git a/change-beta/@azure-communication-react-050e392e-7a15-4f97-8415-a0b53cc93fda.json b/change-beta/@azure-communication-react-050e392e-7a15-4f97-8415-a0b53cc93fda.json new file mode 100644 index 00000000000..27dd488119c --- /dev/null +++ b/change-beta/@azure-communication-react-050e392e-7a15-4f97-8415-a0b53cc93fda.json @@ -0,0 +1,9 @@ +{ + "type": "prerelease", + "area": "feature", + "workstream": "Captions", + "comment": "Stablize Captions Banner Component", + "packageName": "@azure/communication-react", + "email": "96077406+carocao-msft@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/common/config/playwright/playwright.config.common.ts b/common/config/playwright/playwright.config.common.ts index d4a5bc5e9ca..a070bbfdab7 100644 --- a/common/config/playwright/playwright.config.common.ts +++ b/common/config/playwright/playwright.config.common.ts @@ -73,7 +73,7 @@ export const config: PlaywrightTestConfig = { }, // components tests use toHaveScreenshot toHaveScreenshot: { - maxDiffPixels: 1, + maxDiffPixels: 3, // make stricter comparison for colors, default value was 0.2 // which didn't catch some color differences for gray colors threshold: 0 diff --git a/packages/calling-component-bindings/src/captionsSelector.ts b/packages/calling-component-bindings/src/captionsSelector.ts index b7bce226da9..78933e73577 100644 --- a/packages/calling-component-bindings/src/captionsSelector.ts +++ b/packages/calling-component-bindings/src/captionsSelector.ts @@ -19,7 +19,7 @@ import { } from './baseSelectors'; import * as reselect from 'reselect'; import { toFlatCommunicationIdentifier } from '@internal/acs-ui-common'; -import { _CaptionsInfo, _SupportedCaptionLanguage, _SupportedSpokenLanguage } from '@internal/react-components'; +import { CaptionsInformation, _SupportedCaptionLanguage, _SupportedSpokenLanguage } from '@internal/react-components'; /** * Selector type for the {@link StartCaptionsButton} component. @@ -96,22 +96,22 @@ export const _captionSettingsSelector: _CaptionSettingsSelector = reselect.creat ); /** * Selector type for the {@link CaptionsBanner} component. - * @internal + * @public */ -export type _CaptionsBannerSelector = ( +export type CaptionsBannerSelector = ( state: CallClientState, props: CallingBaseSelectorProps ) => { - captions: _CaptionsInfo[]; + captions: CaptionsInformation[]; isCaptionsOn: boolean; }; /** * Selector for {@link CaptionsBanner} component. * - * @internal + * @public */ -export const _captionsBannerSelector: _CaptionsBannerSelector = reselect.createSelector( +export const captionsBannerSelector: CaptionsBannerSelector = reselect.createSelector( [getCaptions, getCaptionsStatus, getStartCaptionsInProgress, getRemoteParticipants, getDisplayName, getIdentifier], (captions, isCaptionsFeatureActive, startCaptionsInProgress, remoteParticipants, displayName, identifier) => { const captionsInfo = captions?.map((c, index) => { diff --git a/packages/calling-component-bindings/src/hooks/usePropsFor.ts b/packages/calling-component-bindings/src/hooks/usePropsFor.ts index 8b88ae0e95a..6a186480f8b 100644 --- a/packages/calling-component-bindings/src/hooks/usePropsFor.ts +++ b/packages/calling-component-bindings/src/hooks/usePropsFor.ts @@ -9,7 +9,8 @@ import { DevicesButton, ParticipantList, ScreenShareButton, - VideoGallery + VideoGallery, + CaptionsBanner } from '@internal/react-components'; import { IncomingCallStack } from '@internal/react-components'; @@ -45,6 +46,7 @@ import { ReactionButton } from '@internal/react-components'; import { _ComponentCallingHandlers } from '../handlers/createHandlers'; import { notificationStackSelector, NotificationStackSelector } from '../notificationStackSelector'; import { incomingCallStackSelector, IncomingCallStackSelector } from '../incomingCallStackSelector'; +import { captionsBannerSelector } from '../captionsSelector'; /** * Primary hook to get all hooks necessary for a calling Component. @@ -126,7 +128,9 @@ export type GetSelector JSX.Element | undefine ? RaiseHandButtonSelector : AreEqual extends true ? EmptySelector - : undefined; + : AreEqual extends true + ? EmptySelector + : undefined; /** * Get the selector for a specified component. @@ -178,6 +182,8 @@ const findSelector = (component: (props: any) => JSX.Element | undefined): any = return holdButtonSelector; case IncomingCallStack: return incomingCallStackSelector; + case CaptionsBanner: + return captionsBannerSelector; } return undefined; }; diff --git a/packages/calling-component-bindings/src/index.ts b/packages/calling-component-bindings/src/index.ts index 667ca958acf..43fae48742c 100644 --- a/packages/calling-component-bindings/src/index.ts +++ b/packages/calling-component-bindings/src/index.ts @@ -21,10 +21,10 @@ export { incomingCallStackSelector } from './incomingCallStackSelector'; export type { _StartCaptionsButtonSelector, _CaptionSettingsSelector, - _CaptionsBannerSelector + CaptionsBannerSelector } from './captionsSelector'; -export { _captionsBannerSelector, _startCaptionsButtonSelector, _captionSettingsSelector } from './captionsSelector'; +export { captionsBannerSelector, _startCaptionsButtonSelector, _captionSettingsSelector } from './captionsSelector'; export type { CallingHandlers, CreateDefaultCallingHandlers } from './handlers/createHandlers'; diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index e34cd1bad7c..cbf8a7f465f 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -1816,6 +1816,27 @@ export interface CaptionLanguageStrings { vi: string; } +// @public +export const CaptionsBanner: (props: CaptionsBannerProps) => JSX.Element; + +// @public +export interface CaptionsBannerProps { + captions: CaptionsInformation[]; + captionsOptions?: { + height: 'full' | 'default'; + }; + formFactor?: 'default' | 'compact'; + isCaptionsOn?: boolean; + onRenderAvatar?: OnRenderAvatarCallback; + startCaptionsInProgress?: boolean; + strings?: CaptionsBannerStrings; +} + +// @public +export interface CaptionsBannerStrings { + captionsBannerSpinnerText?: string; +} + // @public (undocumented) export interface CaptionsCallFeatureState { captions: CaptionsInfo[]; @@ -1839,6 +1860,14 @@ export interface CaptionsInfo { timestamp: Date; } +// @public +export type CaptionsInformation = { + id: string; + displayName: string; + captionText: string; + userId?: string; +}; + // @public export type CaptionsOptions = { spokenLanguage: string; @@ -2432,6 +2461,7 @@ export interface ComponentStrings { CameraSitePermissionsDenied: SitePermissionsStrings; CameraSitePermissionsDeniedSafari: SitePermissionsStrings; CameraSitePermissionsRequest: SitePermissionsStrings; + captionsBanner: CaptionsBannerStrings; devicesButton: DevicesButtonStrings; dialpad: DialpadStrings; endCallButton: EndCallButtonStrings; @@ -3289,7 +3319,7 @@ export interface FluentThemeProviderProps { export const fromFlatCommunicationIdentifier: (id: string) => CommunicationIdentifier; // @public -export type GetCallingSelector JSX.Element | undefined> = AreEqual extends true ? VideoGallerySelector : AreEqual extends true ? DevicesButtonSelector : AreEqual extends true ? MicrophoneButtonSelector : AreEqual extends true ? CameraButtonSelector : AreEqual extends true ? ScreenShareButtonSelector : AreEqual extends true ? ParticipantListSelector : AreEqual extends true ? ParticipantsButtonSelector : AreEqual extends true ? EmptySelector : AreEqual extends true ? CallErrorBarSelector : AreEqual extends true ? EmptySelector : AreEqual extends true ? HoldButtonSelector : AreEqual extends true ? NotificationStackSelector : AreEqual extends true ? IncomingCallStackSelector : AreEqual extends true ? RaiseHandButtonSelector : AreEqual extends true ? EmptySelector : undefined; +export type GetCallingSelector JSX.Element | undefined> = AreEqual extends true ? VideoGallerySelector : AreEqual extends true ? DevicesButtonSelector : AreEqual extends true ? MicrophoneButtonSelector : AreEqual extends true ? CameraButtonSelector : AreEqual extends true ? ScreenShareButtonSelector : AreEqual extends true ? ParticipantListSelector : AreEqual extends true ? ParticipantsButtonSelector : AreEqual extends true ? EmptySelector : AreEqual extends true ? CallErrorBarSelector : AreEqual extends true ? EmptySelector : AreEqual extends true ? HoldButtonSelector : AreEqual extends true ? NotificationStackSelector : AreEqual extends true ? IncomingCallStackSelector : AreEqual extends true ? RaiseHandButtonSelector : AreEqual extends true ? EmptySelector : AreEqual extends true ? EmptySelector : undefined; // @public export const getCallingSelector: JSX.Element | undefined>(component: Component) => GetCallingSelector; diff --git a/packages/communication-react/review/stable/communication-react.api.md b/packages/communication-react/review/stable/communication-react.api.md index 0acd96865e9..21a8077cc1e 100644 --- a/packages/communication-react/review/stable/communication-react.api.md +++ b/packages/communication-react/review/stable/communication-react.api.md @@ -1524,6 +1524,27 @@ export interface CaptionLanguageStrings { vi: string; } +// @public +export const CaptionsBanner: (props: CaptionsBannerProps) => JSX.Element; + +// @public +export interface CaptionsBannerProps { + captions: CaptionsInformation[]; + captionsOptions?: { + height: 'full' | 'default'; + }; + formFactor?: 'default' | 'compact'; + isCaptionsOn?: boolean; + onRenderAvatar?: OnRenderAvatarCallback; + startCaptionsInProgress?: boolean; + strings?: CaptionsBannerStrings; +} + +// @public +export interface CaptionsBannerStrings { + captionsBannerSpinnerText?: string; +} + // @public (undocumented) export interface CaptionsCallFeatureState { captions: CaptionsInfo[]; @@ -1547,6 +1568,14 @@ export interface CaptionsInfo { timestamp: Date; } +// @public +export type CaptionsInformation = { + id: string; + displayName: string; + captionText: string; + userId?: string; +}; + // @public export type CaptionsOptions = { spokenLanguage: string; @@ -2073,6 +2102,7 @@ export type ComponentSlotStyle = Omit; // @public export interface ComponentStrings { cameraButton: CameraButtonStrings; + captionsBanner: CaptionsBannerStrings; devicesButton: DevicesButtonStrings; dialpad: DialpadStrings; endCallButton: EndCallButtonStrings; @@ -2846,7 +2876,7 @@ export interface FluentThemeProviderProps { export const fromFlatCommunicationIdentifier: (id: string) => CommunicationIdentifier; // @public -export type GetCallingSelector JSX.Element | undefined> = AreEqual extends true ? VideoGallerySelector : AreEqual extends true ? DevicesButtonSelector : AreEqual extends true ? MicrophoneButtonSelector : AreEqual extends true ? CameraButtonSelector : AreEqual extends true ? ScreenShareButtonSelector : AreEqual extends true ? ParticipantListSelector : AreEqual extends true ? ParticipantsButtonSelector : AreEqual extends true ? EmptySelector : AreEqual extends true ? CallErrorBarSelector : AreEqual extends true ? EmptySelector : AreEqual extends true ? HoldButtonSelector : AreEqual extends true ? NotificationStackSelector : AreEqual extends true ? IncomingCallStackSelector : AreEqual extends true ? RaiseHandButtonSelector : AreEqual extends true ? EmptySelector : undefined; +export type GetCallingSelector JSX.Element | undefined> = AreEqual extends true ? VideoGallerySelector : AreEqual extends true ? DevicesButtonSelector : AreEqual extends true ? MicrophoneButtonSelector : AreEqual extends true ? CameraButtonSelector : AreEqual extends true ? ScreenShareButtonSelector : AreEqual extends true ? ParticipantListSelector : AreEqual extends true ? ParticipantsButtonSelector : AreEqual extends true ? EmptySelector : AreEqual extends true ? CallErrorBarSelector : AreEqual extends true ? EmptySelector : AreEqual extends true ? HoldButtonSelector : AreEqual extends true ? NotificationStackSelector : AreEqual extends true ? IncomingCallStackSelector : AreEqual extends true ? RaiseHandButtonSelector : AreEqual extends true ? EmptySelector : AreEqual extends true ? EmptySelector : undefined; // @public export const getCallingSelector: JSX.Element | undefined>(component: Component) => GetCallingSelector; diff --git a/packages/communication-react/src/index.ts b/packages/communication-react/src/index.ts index 24b2f3adc0c..8cb44d122d1 100644 --- a/packages/communication-react/src/index.ts +++ b/packages/communication-react/src/index.ts @@ -466,3 +466,9 @@ export type { RealTimeTextProps, RealTimeTextStrings } from '../../react-compone export { RealTimeText } from '../../react-components/src/components/RealTimeText'; /* @conditional-compile-remove(media-access) */ export type { MediaAccess } from '../../react-components/src'; +export type { + CaptionsBannerProps, + CaptionsInformation, + CaptionsBannerStrings +} from '../../react-components/src/components/CaptionsBanner'; +export { CaptionsBanner } from '../../react-components/src/components/CaptionsBanner'; diff --git a/packages/react-components/src/components/Caption.tsx b/packages/react-components/src/components/Caption.tsx index 2fca692e0d6..bd63bac87e9 100644 --- a/packages/react-components/src/components/Caption.tsx +++ b/packages/react-components/src/components/Caption.tsx @@ -11,13 +11,13 @@ import { displayNameContainerClassName, iconClassName } from './styles/Captions.style'; -import { _CaptionsInfo } from './CaptionsBanner'; +import { CaptionsInformation } from './CaptionsBanner'; /** * @internal * Props for a single line of caption. */ -export interface _CaptionProps extends _CaptionsInfo { +export interface _CaptionProps extends CaptionsInformation { /** * Optional callback to override render of the avatar. * diff --git a/packages/react-components/src/components/CaptionsBanner.tsx b/packages/react-components/src/components/CaptionsBanner.tsx index abebe0142b5..6aaa808a5f2 100644 --- a/packages/react-components/src/components/CaptionsBanner.tsx +++ b/packages/react-components/src/components/CaptionsBanner.tsx @@ -12,33 +12,59 @@ import { loadingBannerStyles } from './styles/Captions.style'; import { OnRenderAvatarCallback } from '../types'; +import { useLocale } from '../localization'; /** - * @internal + * @public * information required for each line of caption */ -export type _CaptionsInfo = { +export type CaptionsInformation = { + /** + * unique id for each caption + */ id: string; + /** + * speaker's display name + */ displayName: string; + /** + * content of the caption + */ captionText: string; + /** + * id of the speaker + */ userId?: string; }; /** - * @internal + * @public * strings for captions banner */ -export interface _CaptionsBannerStrings { +export interface CaptionsBannerStrings { + /** + * Spinner text for captions banner + */ captionsBannerSpinnerText?: string; } /** - * @internal - * _CaptionsBanner Component Props. + * @public + * CaptionsBanner Component Props. */ -export interface _CaptionsBannerProps { - captions: _CaptionsInfo[]; +export interface CaptionsBannerProps { + /** + * Array of captions to be displayed + */ + captions: CaptionsInformation[]; + /** + * Flag to indicate if captions are on + */ isCaptionsOn?: boolean; + /** + * Flag to indicate if captions are being started + * This is used to show spinner while captions are being started + */ startCaptionsInProgress?: boolean; /** * Optional callback to override render of the avatar. @@ -46,12 +72,18 @@ export interface _CaptionsBannerProps { * @param userId - user Id */ onRenderAvatar?: OnRenderAvatarCallback; - strings?: _CaptionsBannerStrings; + /** + * Optional strings for the component + */ + strings?: CaptionsBannerStrings; /** * Optional form factor for the component. * @defaultValue 'default' */ formFactor?: 'default' | 'compact'; + /** + * Optional options for the component. + */ captionsOptions?: { height: 'full' | 'default'; }; @@ -60,19 +92,20 @@ export interface _CaptionsBannerProps { const SCROLL_OFFSET_ALLOWANCE = 20; /** - * @internal + * @public * A component for displaying a CaptionsBanner with user icon, displayName and captions text. */ -export const _CaptionsBanner = (props: _CaptionsBannerProps): JSX.Element => { +export const CaptionsBanner = (props: CaptionsBannerProps): JSX.Element => { const { captions, isCaptionsOn, startCaptionsInProgress, onRenderAvatar, - strings, formFactor = 'default', captionsOptions } = props; + const localeStrings = useLocale().strings.captionsBanner; + const strings = { ...localeStrings, ...props.strings }; const captionsScrollDivRef = useRef(null); const [isAtBottomOfScroll, setIsAtBottomOfScroll] = useState(true); const theme = useTheme(); diff --git a/packages/react-components/src/components/RealTimeText.tsx b/packages/react-components/src/components/RealTimeText.tsx index 3140702a50b..bbfcb6ec1ac 100644 --- a/packages/react-components/src/components/RealTimeText.tsx +++ b/packages/react-components/src/components/RealTimeText.tsx @@ -17,8 +17,6 @@ import { rttContainerClassName } from './styles/Captions.style'; /* @conditional-compile-remove(rtt) */ -import { _CaptionsInfo } from './CaptionsBanner'; -/* @conditional-compile-remove(rtt) */ import { useLocale } from '../localization'; /* @conditional-compile-remove(rtt) */ diff --git a/packages/react-components/src/localization/LocalizationProvider.tsx b/packages/react-components/src/localization/LocalizationProvider.tsx index c0ab54566c2..f8a3b932a38 100644 --- a/packages/react-components/src/localization/LocalizationProvider.tsx +++ b/packages/react-components/src/localization/LocalizationProvider.tsx @@ -15,7 +15,8 @@ import { ScreenShareButtonStrings, SendBoxStrings, TypingIndicatorStrings, - VideoGalleryStrings + VideoGalleryStrings, + CaptionsBannerStrings } from '../components'; import { NotificationStackStrings } from '../components'; import { RaiseHandButtonStrings } from '../components'; @@ -194,6 +195,8 @@ export interface ComponentStrings { /* @conditional-compile-remove(rtt) */ /** Strings for RealTimeText */ rtt: RealTimeTextStrings; + /** Strings for CaptionsBanner */ + captionsBanner: CaptionsBannerStrings; } /** diff --git a/packages/react-components/src/localization/locales/en-US/strings.json b/packages/react-components/src/localization/locales/en-US/strings.json index 8046d7899fb..531c9c7b263 100644 --- a/packages/react-components/src/localization/locales/en-US/strings.json +++ b/packages/react-components/src/localization/locales/en-US/strings.json @@ -77,6 +77,9 @@ "rttCancelButtonLabel": "Cancel", "rttCloseModalButtonAriaLabel": "Close RTT Modal" }, + "captionsBanner": { + "captionsBannerSpinnerText": "Starting captions..." + }, "mentionPopover": { "mentionPopoverHeader": "Suggestions" }, diff --git a/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx b/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx index a0b93c6c727..ff5a6a3214c 100644 --- a/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx +++ b/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx @@ -21,7 +21,7 @@ import React, { useMemo, useRef, useState } from 'react'; import { useEffect } from 'react'; import { useCallback } from 'react'; import { AvatarPersonaDataCallback } from '../../common/AvatarPersona'; -import { CaptionsBanner } from '../../common/CaptionsBanner'; +import { CallingCaptionsBanner } from '../../common/CallingCaptionsBanner'; import { containerDivStyles } from '../../common/ContainerRectProps'; import { compositeMinWidthRem } from '../../common/styles/Composite.styles'; import { useAdapter } from '../adapter/CallAdapterProvider'; @@ -609,7 +609,7 @@ export const CallArrangement = (props: CallArrangementProps): JSX.Element => { {renderGallery && props.onRenderGalleryContent && props.onRenderGalleryContent()} {!isInLocalHold && ( - ; }): JSX.Element => { - const captionsBannerProps = useAdaptedSelector(_captionsBannerSelector); - - const handlers = useHandlers(_CaptionsBanner); - + const captionsBannerProps = usePropsFor(CaptionsBanner); const [isCaptionsSettingsOpen, setIsCaptionsSettingsOpen] = useState(false); const onClickCaptionsSettings = (): void => { @@ -60,7 +55,7 @@ export const CaptionsBanner = (props: { const strings = useLocale().strings.call; - const captionsBannerStrings: _CaptionsBannerStrings = { + const captionsBannerStrings: CaptionsBannerStrings = { captionsBannerSpinnerText: strings.captionsBannerSpinnerText }; @@ -99,13 +94,13 @@ export const CaptionsBanner = (props: {
- <_CaptionsBanner - {...captionsBannerProps} - {...handlers} + diff --git a/packages/storybook8/stories/INTERNAL/CaptionsBanner/CaptionsBanner.story.tsx b/packages/storybook8/stories/Components/CaptionsBanner/CaptionsBanner.story.tsx similarity index 86% rename from packages/storybook8/stories/INTERNAL/CaptionsBanner/CaptionsBanner.story.tsx rename to packages/storybook8/stories/Components/CaptionsBanner/CaptionsBanner.story.tsx index 84a40956033..a2f4b31acc0 100644 --- a/packages/storybook8/stories/INTERNAL/CaptionsBanner/CaptionsBanner.story.tsx +++ b/packages/storybook8/stories/Components/CaptionsBanner/CaptionsBanner.story.tsx @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { CaptionsInformation, CaptionsBanner as CaptionsBannerComponent } from '@azure/communication-react'; import { PrimaryButton, Stack } from '@fluentui/react'; -import { _CaptionsInfo, _CaptionsBanner } from '@internal/react-components'; import React, { useState } from 'react'; import { GenerateMockNewCaption, @@ -12,7 +12,7 @@ import { } from './mockCaptions'; const CaptionsBannerStory = (): JSX.Element => { - const [captions, setCaptions] = useState<_CaptionsInfo[]>(GenerateMockNewCaptions()); + const [captions, setCaptions] = useState(GenerateMockNewCaptions()); const addNewCaption = (): void => { setCaptions([...captions, GenerateMockNewCaption()]); @@ -41,7 +41,7 @@ const CaptionsBannerStory = (): JSX.Element => { - <_CaptionsBanner captions={captions} isCaptionsOn startCaptionsInProgress /> + diff --git a/packages/storybook8/stories/INTERNAL/CaptionsBanner/Docs.mdx b/packages/storybook8/stories/Components/CaptionsBanner/Docs.mdx similarity index 82% rename from packages/storybook8/stories/INTERNAL/CaptionsBanner/Docs.mdx rename to packages/storybook8/stories/Components/CaptionsBanner/Docs.mdx index f18fc22349c..7117ebae0bb 100644 --- a/packages/storybook8/stories/INTERNAL/CaptionsBanner/Docs.mdx +++ b/packages/storybook8/stories/Components/CaptionsBanner/Docs.mdx @@ -1,4 +1,4 @@ -import { _CaptionsBanner } from '@internal/react-components'; +import { CaptionsBanner } from '@azure/communication-react'; import { Canvas, Meta, ArgTypes } from '@storybook/blocks'; import * as CaptionsBannerStories from './index.stories'; import CaptionsBannerExampleText from '!!raw-loader!./snippets/CaptionsBanner.snippet.tsx'; @@ -13,4 +13,4 @@ A component for displaying a CaptionsBanner with user icon, displayName and capt ## Props - + diff --git a/packages/storybook8/stories/INTERNAL/CaptionsBanner/index.stories.tsx b/packages/storybook8/stories/Components/CaptionsBanner/index.stories.tsx similarity index 54% rename from packages/storybook8/stories/INTERNAL/CaptionsBanner/index.stories.tsx rename to packages/storybook8/stories/Components/CaptionsBanner/index.stories.tsx index 0ea6d45f62c..e2308efc719 100644 --- a/packages/storybook8/stories/INTERNAL/CaptionsBanner/index.stories.tsx +++ b/packages/storybook8/stories/Components/CaptionsBanner/index.stories.tsx @@ -1,19 +1,23 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { _CaptionsBanner } from '@internal/react-components'; +import { CaptionsBanner } from '@azure/communication-react'; import { Meta } from '@storybook/react'; import { hiddenControl } from '../../controlsUtils'; export { CaptionsBanner } from './CaptionsBanner.story'; const meta: Meta = { - title: 'Components/Internal/Captions Banner', - component: _CaptionsBanner, + title: 'Components/Captions Banner', + component: CaptionsBanner, argTypes: { captions: hiddenControl, + isCaptionsOn: hiddenControl, + startCaptionsInProgress: hiddenControl, + strings: hiddenControl, onRenderAvatar: hiddenControl, - isCaptionsOn: hiddenControl + formFactor: hiddenControl, + captionsOptions: hiddenControl } }; diff --git a/packages/storybook8/stories/INTERNAL/CaptionsBanner/mockCaptions.ts b/packages/storybook8/stories/Components/CaptionsBanner/mockCaptions.ts similarity index 83% rename from packages/storybook8/stories/INTERNAL/CaptionsBanner/mockCaptions.ts rename to packages/storybook8/stories/Components/CaptionsBanner/mockCaptions.ts index 48f64e0338e..786fa5f0fb5 100644 --- a/packages/storybook8/stories/INTERNAL/CaptionsBanner/mockCaptions.ts +++ b/packages/storybook8/stories/Components/CaptionsBanner/mockCaptions.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { _CaptionsInfo } from '@internal/react-components'; +import { CaptionsInformation } from '@azure/communication-react'; -export const GenerateMockNewCaption = (): _CaptionsInfo => { +export const GenerateMockNewCaption = (): CaptionsInformation => { return { id: Date.now().toString(), displayName: 'SpongeBob', @@ -12,7 +12,7 @@ export const GenerateMockNewCaption = (): _CaptionsInfo => { }; }; -export const GenerateMockNewShortCaption = (): _CaptionsInfo => { +export const GenerateMockNewShortCaption = (): CaptionsInformation => { return { id: Date.now().toString(), displayName: 'SpongeBob Patrick', @@ -20,7 +20,7 @@ export const GenerateMockNewShortCaption = (): _CaptionsInfo => { }; }; -export const GenerateMockNewCaptionWithLongName = (): _CaptionsInfo => { +export const GenerateMockNewCaptionWithLongName = (): CaptionsInformation => { return { id: Date.now().toString(), displayName: 'SpongeBob Patrick Robert', @@ -28,7 +28,7 @@ export const GenerateMockNewCaptionWithLongName = (): _CaptionsInfo => { }; }; -export const GenerateMockNewCaptions = (): _CaptionsInfo[] => { +export const GenerateMockNewCaptions = (): CaptionsInformation[] => { return [ { id: Date.now().toString(), diff --git a/packages/storybook8/stories/INTERNAL/CaptionsBanner/snippets/CaptionsBanner.snippet.tsx b/packages/storybook8/stories/Components/CaptionsBanner/snippets/CaptionsBanner.snippet.tsx similarity index 86% rename from packages/storybook8/stories/INTERNAL/CaptionsBanner/snippets/CaptionsBanner.snippet.tsx rename to packages/storybook8/stories/Components/CaptionsBanner/snippets/CaptionsBanner.snippet.tsx index c2b8ac46eca..da03be6ec71 100644 --- a/packages/storybook8/stories/INTERNAL/CaptionsBanner/snippets/CaptionsBanner.snippet.tsx +++ b/packages/storybook8/stories/Components/CaptionsBanner/snippets/CaptionsBanner.snippet.tsx @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { CaptionsInformation, CaptionsBanner } from '@azure/communication-react'; import { PrimaryButton, Stack } from '@fluentui/react'; -import { _CaptionsInfo, _CaptionsBanner } from '@internal/react-components'; import React, { useState } from 'react'; import { GenerateMockNewCaption, @@ -12,7 +12,7 @@ import { } from '../mockCaptions'; export const CaptionsBannerStory = (): JSX.Element => { - const [captions, setCaptions] = useState<_CaptionsInfo[]>(GenerateMockNewCaptions()); + const [captions, setCaptions] = useState(GenerateMockNewCaptions()); const addNewCaption = (): void => { setCaptions([...captions, GenerateMockNewCaption()]); @@ -44,7 +44,7 @@ export const CaptionsBannerStory = (): JSX.Element => { - <_CaptionsBanner captions={captions} isCaptionsOn startCaptionsInProgress /> + From ccded9cd10f1815f1a47b63b643ede8f86a769fa Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Sat, 7 Dec 2024 22:40:45 +0000 Subject: [PATCH 19/37] Updated API --- .../src/baseSelectors.ts | 3 +-- .../src/videoGallerySelector.ts | 15 ++++++++------- .../calling-stateful-client/src/index-public.ts | 3 +-- .../review/beta/communication-react.api.md | 8 ++++---- .../src/components/VideoGallery.tsx | 8 +++++++- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/calling-component-bindings/src/baseSelectors.ts b/packages/calling-component-bindings/src/baseSelectors.ts index e18abfdbf07..4b51d1208fd 100644 --- a/packages/calling-component-bindings/src/baseSelectors.ts +++ b/packages/calling-component-bindings/src/baseSelectors.ts @@ -27,8 +27,7 @@ import { ConferencePhoneInfo } from '@internal/calling-stateful-client'; /* @conditional-compile-remove(breakout-rooms) */ import { CallNotifications } from '@internal/calling-stateful-client'; /* @conditional-compile-remove(together-mode) */ -import { TogetherModeCallFeatureState } from '@internal/calling-stateful-client/dist/dist-esm/CallClientState'; - +import { TogetherModeCallFeatureState } from '@internal/calling-stateful-client'; /** * Common props used to reference calling declarative client state. * diff --git a/packages/calling-component-bindings/src/videoGallerySelector.ts b/packages/calling-component-bindings/src/videoGallerySelector.ts index fc885ca6909..6468d8f45d6 100644 --- a/packages/calling-component-bindings/src/videoGallerySelector.ts +++ b/packages/calling-component-bindings/src/videoGallerySelector.ts @@ -3,11 +3,12 @@ import { toFlatCommunicationIdentifier } from '@internal/acs-ui-common'; import { CallClientState, RemoteParticipantState } from '@internal/calling-stateful-client'; -/* @conditional-compile-remove(together-mode) */ -import { TogetherModeParticipantSeatingState, TogetherModeStreamsState } from '@internal/calling-stateful-client'; import { VideoGalleryRemoteParticipant, VideoGalleryLocalParticipant } from '@internal/react-components'; /* @conditional-compile-remove(together-mode) */ -import { VideoGalleryTogetherModeStreams } from '@internal/react-components'; +import { + VideoGalleryTogetherModeStreams, + VideoGalleryTogetherModeParticipantPosition +} from '@internal/react-components'; import { createSelector } from 'reselect'; import { CallingBaseSelectorProps, @@ -60,9 +61,9 @@ export type VideoGallerySelector = ( /* @conditional-compile-remove(together-mode) */ startTogetherModeEnabled?: boolean; /* @conditional-compile-remove(together-mode) */ - togetherModeStreamsMap?: TogetherModeStreamsState; + togetherModeStreams?: VideoGalleryTogetherModeStreams; /* @conditional-compile-remove(together-mode) */ - togetherModeSeatingCoordinates?: TogetherModeParticipantSeatingState; + togetherModeSeatingCoordinates?: VideoGalleryTogetherModeParticipantPosition; }; /** @@ -121,7 +122,7 @@ export const videoGallerySelector: VideoGallerySelector = createSelector( const localParticipantReactionState = memoizedConvertToVideoTileReaction(localParticipantReaction); const spotlightedParticipantIds = memoizeSpotlightedParticipantIds(spotlightCallFeature?.spotlightedParticipants); /* @conditional-compile-remove(together-mode) */ - const togetherModeStreamsMap: VideoGalleryTogetherModeStreams = { + const togetherModeStreams = { mainVideoStream: { isAvailable: togetherModeCallFeature?.streams?.mainVideoStream?.isAvailable, renderElement: togetherModeCallFeature?.streams?.mainVideoStream?.view?.target, @@ -166,7 +167,7 @@ export const videoGallerySelector: VideoGallerySelector = createSelector( spotlightedParticipants: spotlightedParticipantIds, maxParticipantsToSpotlight: spotlightCallFeature?.maxParticipantsToSpotlight, /* @conditional-compile-remove(together-mode) */ - togetherModeStreams: togetherModeStreamsMap, + togetherModeStreams: togetherModeStreams, /* @conditional-compile-remove(together-mode) */ togetherModeSeatingCoordinates: togetherModeCallFeature?.seatingPositions, /* @conditional-compile-remove(together-mode) */ diff --git a/packages/calling-stateful-client/src/index-public.ts b/packages/calling-stateful-client/src/index-public.ts index cd74768190a..d3c71dea25b 100644 --- a/packages/calling-stateful-client/src/index-public.ts +++ b/packages/calling-stateful-client/src/index-public.ts @@ -32,9 +32,8 @@ export type { RemoteDiagnosticState, RemoteDiagnosticType } from './CallClientSt export type { CreateViewResult } from './StreamUtils'; export type { RaiseHandCallFeatureState as RaiseHandCallFeature } from './CallClientState'; /* @conditional-compile-remove(together-mode) */ -export type { TogetherModeCallFeatureState as TogetherModeCallFeature } from './CallClientState'; -/* @conditional-compile-remove(together-mode) */ export type { + TogetherModeCallFeatureState, CallFeatureStreamState, TogetherModeSeatingPositionState, CallFeatureStreamName, diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index cbf8a7f465f..61d40991998 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -1193,7 +1193,7 @@ export interface CallState { startTime: Date; state: CallState_2; // @beta - togetherMode: TogetherModeCallFeature; + togetherMode: TogetherModeCallFeatureState; totalParticipantCount?: number; transcription: TranscriptionCallFeature; transfer: TransferFeature; @@ -5077,7 +5077,7 @@ export type TeamsOutboundCallAdapterArgs = TeamsCallAdapterArgsCommon & { export const toFlatCommunicationIdentifier: (identifier: CommunicationIdentifier) => string; // @beta -export interface TogetherModeCallFeature { +export interface TogetherModeCallFeatureState { // (undocumented) isActive: boolean; seatingPositions: TogetherModeParticipantSeatingState; @@ -5436,8 +5436,8 @@ export type VideoGallerySelector = (state: CallClientState, props: CallingBaseSe maxParticipantsToSpotlight?: number; isTogetherModeActive?: boolean; startTogetherModeEnabled?: boolean; - togetherModeStreamsMap?: TogetherModeStreamsState; - togetherModeSeatingCoordinates?: TogetherModeParticipantSeatingState; + togetherModeStreams?: VideoGalleryTogetherModeStreams; + togetherModeSeatingCoordinates?: VideoGalleryTogetherModeParticipantPosition; }; // @public diff --git a/packages/react-components/src/components/VideoGallery.tsx b/packages/react-components/src/components/VideoGallery.tsx index 94904216773..fe8d92c7ff4 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -790,6 +790,7 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { containerHeight ] ); + /* @conditional-compile-remove(together-mode) */ // Current implementation of capabilities is only based on user role. // This logic checks for the user role and if the user is a Teams user. const canSwitchToTogetherModeLayout = @@ -859,7 +860,12 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { return ; } return ; - }, [canSwitchToTogetherModeLayout, layout, layoutProps, screenShareParticipant]); + }, [ + /* @conditional-compile-remove(together-mode) */ canSwitchToTogetherModeLayout, + layout, + layoutProps, + screenShareParticipant + ]); return (
Date: Sun, 8 Dec 2024 02:30:00 +0000 Subject: [PATCH 20/37] Hover effect on together mode --- .../src/components/MeetingReactionOverlay.tsx | 1 - .../src/components/TogetherModeOverlay.tsx | 20 +++++++++++++++---- .../src/components/VideoGallery.tsx | 13 ++++-------- .../VideoGallery/TogetherModeStream.tsx | 11 +++------- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/packages/react-components/src/components/MeetingReactionOverlay.tsx b/packages/react-components/src/components/MeetingReactionOverlay.tsx index 2243c4639c5..61c72c762f4 100644 --- a/packages/react-components/src/components/MeetingReactionOverlay.tsx +++ b/packages/react-components/src/components/MeetingReactionOverlay.tsx @@ -139,7 +139,6 @@ export const MeetingReactionOverlay = (props: MeetingReactionOverlayProps): JSX. style={{ width: '100%', height: '100%', - pointerEvents: 'none', position: 'absolute', top: '0', left: '0' diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index 772230ba07b..c4202f2e8b5 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -72,6 +72,7 @@ export const TogetherModeOverlay = React.memo( const locale = useLocale(); const { reactionResources, remoteParticipants, localParticipant, participantsSeatingArrangement } = props; const [visibleSignals, setVisibleSignals] = useState>({}); + const [hoveredParticipantID, setHoveredParticipantID] = useState(''); useMemo(() => { const updatedParticipantsIds = remoteParticipants?.map((participant) => participant.userId) ?? []; @@ -104,12 +105,17 @@ export const TogetherModeOverlay = React.memo( isSpotlighted: !!participant.spotlight, isMuted: participant.isMuted, displayName: participant.displayName || locale.strings.videoGallery.displayNamePlaceholder, - showDisplayName: !!(participant.spotlight || participant.raisedHand || participant.reaction), + showDisplayName: !!( + participant.spotlight || + participant.raisedHand || + participant.reaction || + hoveredParticipantID === participantID + ), seatPositionStyle: togetherModeSeatStyle } })); }, - [locale.strings.videoGallery.displayNamePlaceholder, participantsSeatingArrangement] + [hoveredParticipantID, locale.strings.videoGallery.displayNamePlaceholder, participantsSeatingArrangement] ); useEffect(() => { @@ -131,7 +137,6 @@ export const TogetherModeOverlay = React.memo( style={{ width: '100%', height: '100%', - pointerEvents: 'none', position: 'absolute', top: '0', left: '0' @@ -145,7 +150,14 @@ export const TogetherModeOverlay = React.memo( border: '1px solid red', position: 'absolute', left: `${participantSignal.seatPositionStyle.seatCoordinates.left}px`, - top: `${participantSignal.seatPositionStyle.seatCoordinates.top}px` + top: `${participantSignal.seatPositionStyle.seatCoordinates.top}px`, + zIndex: 70 + }} + onMouseEnter={() => { + setHoveredParticipantID(`${participantSignal.id}`); + }} + onMouseLeave={() => { + setHoveredParticipantID(''); }} >
diff --git a/packages/react-components/src/components/VideoGallery.tsx b/packages/react-components/src/components/VideoGallery.tsx index fe8d92c7ff4..aeb17e39be9 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -793,8 +793,8 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { /* @conditional-compile-remove(together-mode) */ // Current implementation of capabilities is only based on user role. // This logic checks for the user role and if the user is a Teams user. - const canSwitchToTogetherModeLayout = - isTogetherModeActive || (_isIdentityMicrosoftTeamsUser(localParticipant.userId) && startTogetherModeEnabled); + // const canSwitchToTogetherModeLayout = + // isTogetherModeActive || (_isIdentityMicrosoftTeamsUser(localParticipant.userId) && startTogetherModeEnabled); const layoutProps = useMemo( () => ({ @@ -856,16 +856,11 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { /* @conditional-compile-remove(together-mode) */ // Teams users can switch to Together mode layout only if they have the capability, // while ACS users can do so only if Together mode is enabled. - if (layout === 'togetherMode' && canSwitchToTogetherModeLayout) { + if (layout === 'togetherMode') { return ; } return ; - }, [ - /* @conditional-compile-remove(together-mode) */ canSwitchToTogetherModeLayout, - layout, - layoutProps, - screenShareParticipant - ]); + }, [layout, layoutProps, screenShareParticipant]); return (
{ - if (!onSetTogetherModeSceneSize) { - console.log(`Chuk Scene change callback is null ${containerWidth} ${containerHeight}`); - } if (onSetTogetherModeSceneSize && containerWidth && containerHeight) { if (containerWidth !== sceneDimensions.width || containerHeight !== sceneDimensions.height) { onSetTogetherModeSceneSize(containerWidth, containerHeight); - console.log(`Setting scene size width to ${containerWidth} == ${sceneDimensions.width}`); - console.log(`Setting scene size height to ${containerHeight} == ${sceneDimensions.height}`); setSceneDimensions({ width: containerWidth, height: containerHeight }); } } - }, [onSetTogetherModeSceneSize, containerWidth, containerHeight, sceneDimensions.width, sceneDimensions.height]); + }, [onSetTogetherModeSceneSize, containerWidth, containerHeight, sceneDimensions]); // Memoized layout computation const layout = getTogetherModeMainVideoLayout(props); @@ -122,7 +117,7 @@ const getTogetherModeMainVideoLayout = (props: { videoStreamElement={stream?.renderElement || null} isMirrored={true} loadingState={showLoadingIndicator ? 'loading' : 'none'} - styles={togetherModeRootStyle(props.containerWidth, props.containerHeight)} + // styles={togetherModeRootStyle(props.containerWidth, props.containerHeight)} /> {props.reactionResources && ( Date: Sun, 8 Dec 2024 05:19:51 +0000 Subject: [PATCH 21/37] reaction animation --- .../src/components/TogetherModeOverlay.tsx | 108 +++++++++++------- .../components/styles/TogetherMode.styles.ts | 19 +++ 2 files changed, 84 insertions(+), 43 deletions(-) diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index c4202f2e8b5..fe0b7b32603 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -12,7 +12,7 @@ import { VideoGalleryRemoteParticipant } from '../types'; /* @conditional-compile-remove(together-mode) */ -import { spriteAnimationStyles } from './styles/ReactionOverlay.style'; +import { moveAnimationStyles, spriteAnimationStyles } from './styles/ReactionOverlay.style'; /* @conditional-compile-remove(together-mode) */ import { // getCombinedKey, @@ -29,6 +29,7 @@ import { useLocale } from '../localization'; import { _HighContrastAwareIcon } from './HighContrastAwareIcon'; /* @conditional-compile-remove(together-mode) */ import { + ellipsisTextStyle, getTogetherModeParticipantOverlayStyle, getTogetherModeSeatPositionStyle, ITogetherModeSeatPositionStyle @@ -150,8 +151,8 @@ export const TogetherModeOverlay = React.memo( border: '1px solid red', position: 'absolute', left: `${participantSignal.seatPositionStyle.seatCoordinates.left}px`, - top: `${participantSignal.seatPositionStyle.seatCoordinates.top}px`, - zIndex: 70 + top: `${participantSignal.seatPositionStyle.seatCoordinates.top}px` + // zIndex: 70 }} onMouseEnter={() => { setHoveredParticipantID(`${participantSignal.id}`); @@ -161,50 +162,71 @@ export const TogetherModeOverlay = React.memo( }} >
- { + {participantSignal?.reaction?.reactionType && (
+ style={{ + width: `${REACTION_START_DISPLAY_SIZE}px`, + left: `${(100 - (REACTION_START_DISPLAY_SIZE / (participantSignal.seatPositionStyle.seatCoordinates.width ?? 1)) * 100) / 2}%`, + position: 'absolute' + }} + > +
+
- } -
- - - {participantSignal.isHandRaised && ( - - - - )} - {participantSignal.showDisplayName && ( - - {participantSignal.displayName} - - )} - {participantSignal.showDisplayName && participantSignal.isMuted && ( - - - - )} - {participantSignal.isSpotlighted && ( - - - - )} - + )} +
+ + {participantSignal.isHandRaised && ( + + + + )} + {participantSignal.showDisplayName && ( + + {participantSignal.displayName} + + )} + {participantSignal.showDisplayName && participantSignal.isMuted && ( + + + + )} + {participantSignal.isSpotlighted && ( + + + + )}
diff --git a/packages/react-components/src/components/styles/TogetherMode.styles.ts b/packages/react-components/src/components/styles/TogetherMode.styles.ts index 6dc73d8724f..2e3d058df82 100644 --- a/packages/react-components/src/components/styles/TogetherMode.styles.ts +++ b/packages/react-components/src/components/styles/TogetherMode.styles.ts @@ -6,6 +6,7 @@ import { _pxToRem } from '@internal/acs-ui-common'; import { BaseCustomStyles } from '../../types/CustomStylesProps'; /* @conditional-compile-remove(together-mode) */ import { VideoGalleryTogetherModeSeatingInfo } from '../../types/TogetherModeTypes'; +import { mergeStyles } from '@fluentui/react'; /* @conditional-compile-remove(together-mode) */ /** @@ -113,3 +114,21 @@ export function togetherModeHover(): React.CSSProperties { border: '1px solid blue' }; } + +/** + * Style for text with ellipsis overflow. + */ +export const ellipsisTextStyle = mergeStyles({ + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + maxWidth: '150px', // Initial max width + display: 'inline-block', + transition: 'all 0.3s ease-in-out', // Smooth transition for all changes + + selectors: { + '&:hover': { + maxWidth: '300px' // Expanded width on hover + } + } +}); From aaa380b0457ad1634239165ee1b99ea425dfee96 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Mon, 9 Dec 2024 00:55:11 +0000 Subject: [PATCH 22/37] Display name auto expand --- .../src/components/TogetherModeOverlay.tsx | 21 ++++++++++++++----- .../src/components/VideoGallery.tsx | 13 ++++++++---- .../components/styles/TogetherMode.styles.ts | 7 ++++--- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index fe0b7b32603..f5a33c4d85f 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -152,7 +152,6 @@ export const TogetherModeOverlay = React.memo( position: 'absolute', left: `${participantSignal.seatPositionStyle.seatCoordinates.left}px`, top: `${participantSignal.seatPositionStyle.seatCoordinates.top}px` - // zIndex: 70 }} onMouseEnter={() => { setHoveredParticipantID(`${participantSignal.id}`); @@ -196,10 +195,22 @@ export const TogetherModeOverlay = React.memo( backgroundColor: participantSignal.showDisplayName ? 'rgba(0, 0, 0, 0.7)' : 'transparent', color: 'white', textAlign: 'center', - overflow: 'visible' // Allow content to overflow parent + fontSize: '4px' }} > - + {participantSignal.isHandRaised && ( @@ -210,8 +221,8 @@ export const TogetherModeOverlay = React.memo( data-ui-id="video-tile-display-name" className={ellipsisTextStyle} style={{ - maxWidth: hoveredParticipantID === `${participantSignal.id}` ? '300px' : '150px', // Dynamically change width based on state - overflow: hoveredParticipantID === `${participantSignal.id}` ? 'visible' : 'hidden' // Show content when expanded + overflow: hoveredParticipantID === `${participantSignal.id}` ? 'visible' : 'hidden', // Show content when expanded + transition: 'width 0.3s ease' // Smooth transition for width changes }} > {participantSignal.displayName} diff --git a/packages/react-components/src/components/VideoGallery.tsx b/packages/react-components/src/components/VideoGallery.tsx index aeb17e39be9..fe8d92c7ff4 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -793,8 +793,8 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { /* @conditional-compile-remove(together-mode) */ // Current implementation of capabilities is only based on user role. // This logic checks for the user role and if the user is a Teams user. - // const canSwitchToTogetherModeLayout = - // isTogetherModeActive || (_isIdentityMicrosoftTeamsUser(localParticipant.userId) && startTogetherModeEnabled); + const canSwitchToTogetherModeLayout = + isTogetherModeActive || (_isIdentityMicrosoftTeamsUser(localParticipant.userId) && startTogetherModeEnabled); const layoutProps = useMemo( () => ({ @@ -856,11 +856,16 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { /* @conditional-compile-remove(together-mode) */ // Teams users can switch to Together mode layout only if they have the capability, // while ACS users can do so only if Together mode is enabled. - if (layout === 'togetherMode') { + if (layout === 'togetherMode' && canSwitchToTogetherModeLayout) { return ; } return ; - }, [layout, layoutProps, screenShareParticipant]); + }, [ + /* @conditional-compile-remove(together-mode) */ canSwitchToTogetherModeLayout, + layout, + layoutProps, + screenShareParticipant + ]); return (
Date: Wed, 18 Dec 2024 17:56:21 +0000 Subject: [PATCH 23/37] 12/19 - demo commit --- .../src/videoGallerySelector.ts | 1 + .../src/CallContext.ts | 4 +- .../review/beta/communication-react.api.md | 4 + .../src/components/MeetingReactionOverlay.tsx | 9 +- .../src/components/TogetherModeOverlay.tsx | 317 +++++++++++------- .../src/components/VideoGallery.tsx | 3 +- .../VideoGallery/TogetherModeLayout.tsx | 1 + .../VideoGallery/TogetherModeStream.tsx | 109 +++--- .../components/styles/TogetherMode.styles.ts | 142 ++++++-- .../react-components/src/components/utils.ts | 6 +- packages/react-components/src/index.ts | 9 - .../localization/locales/en-US/strings.json | 2 - .../react-components/src/theming/icons.tsx | 6 +- .../common/ControlBar/DesktopMoreButton.tsx | 2 +- .../src/composites/common/icons.tsx | 7 +- 15 files changed, 391 insertions(+), 231 deletions(-) diff --git a/packages/calling-component-bindings/src/videoGallerySelector.ts b/packages/calling-component-bindings/src/videoGallerySelector.ts index c31a7da25c7..1d0d2bedbe0 100644 --- a/packages/calling-component-bindings/src/videoGallerySelector.ts +++ b/packages/calling-component-bindings/src/videoGallerySelector.ts @@ -124,6 +124,7 @@ export const videoGallerySelector: VideoGallerySelector = createSelector( /* @conditional-compile-remove(together-mode) */ const togetherModeStreams = { mainVideoStream: { + isReceiving: togetherModeCallFeature?.streams?.mainVideoStream?.isReceiving, isAvailable: togetherModeCallFeature?.streams?.mainVideoStream?.isAvailable, renderElement: togetherModeCallFeature?.streams?.mainVideoStream?.view?.target, streamSize: togetherModeCallFeature?.streams?.mainVideoStream?.streamSize diff --git a/packages/calling-stateful-client/src/CallContext.ts b/packages/calling-stateful-client/src/CallContext.ts index e383999f226..c4abe43a11e 100644 --- a/packages/calling-stateful-client/src/CallContext.ts +++ b/packages/calling-stateful-client/src/CallContext.ts @@ -559,7 +559,9 @@ export class CallContext { for (const [userId, seatingPosition] of seatingMap.entries()) { seatingPositions[userId] = seatingPosition; } - call.togetherMode.seatingPositions = seatingPositions; + if (Object.keys(seatingPositions).length > 0) { + call.togetherMode.seatingPositions = seatingPositions; + } } }); } diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index dbfdabd5ef4..592918d3745 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -732,6 +732,7 @@ export type CallCompositeIcons = { JoinByPhoneWaitToBeAdmittedIcon?: JSX.Element; PeoplePaneMoreButton?: JSX.Element; StopAllSpotlightMenuButton?: JSX.Element; + TogetherModeLayout?: JSX.Element; }; // @public @@ -2959,6 +2960,7 @@ export const DEFAULT_COMPONENT_ICONS: { IncomingCallNotificationAcceptIcon: React_2.JSX.Element; IncomingCallNotificationAcceptWithVideoIcon: React_2.JSX.Element; RTTIcon: React_2.JSX.Element; + NotificationBarTogetherModeIcon: React_2.JSX.Element; }; // @public @@ -3054,6 +3056,7 @@ export const DEFAULT_COMPOSITE_ICONS: { JoinByPhoneWaitToBeAdmittedIcon?: JSX.Element | undefined; PeoplePaneMoreButton?: JSX.Element | undefined; StopAllSpotlightMenuButton?: JSX.Element | undefined; + TogetherModeLayout?: JSX.Element | undefined; ChevronLeft?: JSX.Element | undefined; ControlBarChatButtonActive?: JSX.Element | undefined; ControlBarChatButtonInactive?: JSX.Element | undefined; @@ -3140,6 +3143,7 @@ export const DEFAULT_COMPOSITE_ICONS: { IncomingCallNotificationAcceptIcon: React_2.JSX.Element; IncomingCallNotificationAcceptWithVideoIcon: React_2.JSX.Element; RTTIcon: React_2.JSX.Element; + NotificationBarTogetherModeIcon: React_2.JSX.Element; }; // @beta diff --git a/packages/react-components/src/components/MeetingReactionOverlay.tsx b/packages/react-components/src/components/MeetingReactionOverlay.tsx index 61c72c762f4..72f36953ad3 100644 --- a/packages/react-components/src/components/MeetingReactionOverlay.tsx +++ b/packages/react-components/src/components/MeetingReactionOverlay.tsx @@ -75,7 +75,7 @@ const REACTION_EMOJI_RESIZE_SCALE_CONSTANT = 3; * @internal */ export const MeetingReactionOverlay = (props: MeetingReactionOverlayProps): JSX.Element => { - const { overlayMode, reaction, reactionResources, localParticipant, remoteParticipants } = props; + const { overlayMode, reaction, reactionResources, localParticipant, remoteParticipants, seatingCoordinates } = props; const [emojiSizePx, setEmojiSizePx] = useState(0); const [divHeight, setDivHeight] = useState(0); const [divWidth, setDivWidth] = useState(0); @@ -136,6 +136,7 @@ export const MeetingReactionOverlay = (props: MeetingReactionOverlayProps): JSX. /* @conditional-compile-remove(together-mode) */ return (
); diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index f5a33c4d85f..382332f662a 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -2,7 +2,7 @@ // Licensed under the MIT License. /* @conditional-compile-remove(together-mode) */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; /* @conditional-compile-remove(together-mode) */ import { Reaction, @@ -16,11 +16,11 @@ import { moveAnimationStyles, spriteAnimationStyles } from './styles/ReactionOve /* @conditional-compile-remove(together-mode) */ import { // getCombinedKey, - REACTION_NUMBER_OF_ANIMATION_FRAMES, - REACTION_START_DISPLAY_SIZE + REACTION_NUMBER_OF_ANIMATION_FRAMES + // REACTION_START_DISPLAY_SIZE } from './VideoGallery/utils/reactionUtils'; /* @conditional-compile-remove(together-mode) */ -import { Icon, mergeStyles, Stack, Text } from '@fluentui/react'; +import { Icon, Text } from '@fluentui/react'; /* @conditional-compile-remove(together-mode) */ import { getEmojiResource } from './VideoGallery/utils/videoGalleryLayoutUtils'; /* @conditional-compile-remove(together-mode) */ @@ -29,13 +29,17 @@ import { useLocale } from '../localization'; import { _HighContrastAwareIcon } from './HighContrastAwareIcon'; /* @conditional-compile-remove(together-mode) */ import { - ellipsisTextStyle, + calculateScaledSize, getTogetherModeParticipantOverlayStyle, getTogetherModeSeatPositionStyle, ITogetherModeSeatPositionStyle } from './styles/TogetherMode.styles'; +import { CallingTheme, useTheme } from '../theming'; +// import { iconContainerStyle, raiseHandContainerStyles } from './styles/VideoTile.styles'; +import { RaisedHandIcon } from './assets/RaisedHandIcon'; /* @conditional-compile-remove(together-mode) */ -import { iconContainerStyle } from './styles/VideoTile.styles'; +// import { iconContainerStyle } from './styles/VideoTile.styles'; +// import { useTheme } from '../theming'; /* @conditional-compile-remove(together-mode) */ /** @@ -50,7 +54,7 @@ type VisibleTogetherModeSignalingAction = { isHandRaised?: boolean; isSpotlighted?: boolean; isMuted?: boolean; - id?: string; + id: string; seatPositionStyle: ITogetherModeSeatPositionStyle; displayName?: string; showDisplayName?: boolean; @@ -64,74 +68,103 @@ type VisibleTogetherModeSignalingAction = { */ export const TogetherModeOverlay = React.memo( (props: { - emojiSize?: number; + emojiSize: number; reactionResources: ReactionResources; - localParticipant?: VideoGalleryLocalParticipant; - remoteParticipants?: VideoGalleryRemoteParticipant[]; - participantsSeatingArrangement?: VideoGalleryTogetherModeParticipantPosition; + localParticipant: VideoGalleryLocalParticipant; + remoteParticipants: VideoGalleryRemoteParticipant[]; + participantsSeatingArrangement: VideoGalleryTogetherModeParticipantPosition; }) => { const locale = useLocale(); - const { reactionResources, remoteParticipants, localParticipant, participantsSeatingArrangement } = props; - const [visibleSignals, setVisibleSignals] = useState>({}); - const [hoveredParticipantID, setHoveredParticipantID] = useState(''); + const theme = useTheme(); + const callingPalette = (theme as unknown as CallingTheme).callingPalette; - useMemo(() => { - const updatedParticipantsIds = remoteParticipants?.map((participant) => participant.userId) ?? []; - updatedParticipantsIds.push(localParticipant?.userId ?? ''); + const { emojiSize, reactionResources, remoteParticipants, localParticipant, participantsSeatingArrangement } = + props; + const [visibleSignals, setVisibleSignals] = useState<{ [key: string]: VisibleTogetherModeSignalingAction }>({}); + const [hoveredParticipantID, setHoveredParticipantID] = useState(''); + const hideSignalForParticipantsNotInTogetherMode = useCallback(() => { const removedVisibleParticipants = Object.keys(visibleSignals).filter( - (participantId) => !updatedParticipantsIds.includes(participantId) + (participantId) => !participantsSeatingArrangement[participantId] ); + // Update visible signals state instead of directly mutating it + setVisibleSignals((prevSignals) => { + const newSignals = { ...prevSignals }; + removedVisibleParticipants.forEach((participantId) => { + delete newSignals[participantId]; + }); - removedVisibleParticipants.forEach((participantId) => { - delete visibleSignals[participantId]; + // Trigger a re-render only if changes occurred + const hasChanges = Object.keys(newSignals).length !== Object.keys(prevSignals).length; + if (hasChanges) { + return newSignals; + } + return prevSignals; }); - }, [remoteParticipants, localParticipant, visibleSignals]); + }, [visibleSignals, participantsSeatingArrangement]); - const updateTogetherModeSeatingUI = useCallback( - (participant: VideoGalleryLocalParticipant | VideoGalleryRemoteParticipant): void => { - const participantID = participant.userId; - const seatingPosition = participantsSeatingArrangement?.[participantID]; - if (!seatingPosition) { - return; - } - const togetherModeSeatStyle: ITogetherModeSeatPositionStyle = getTogetherModeSeatPositionStyle(seatingPosition); + const Testing = useCallback(() => { + const allParticipants = [...remoteParticipants, localParticipant]; - setVisibleSignals((prevVisibleSignals) => ({ - ...prevVisibleSignals, - [participantID]: { - id: participantID, - reaction: participant.reaction, - isHandRaised: !!participant.raisedHand, - isSpotlighted: !!participant.spotlight, - isMuted: participant.isMuted, - displayName: participant.displayName || locale.strings.videoGallery.displayNamePlaceholder, - showDisplayName: !!( - participant.spotlight || - participant.raisedHand || - participant.reaction || - hoveredParticipantID === participantID - ), - seatPositionStyle: togetherModeSeatStyle + const participantsWithVideoAvailable = allParticipants.filter( + (p) => p.videoStream?.isAvailable && participantsSeatingArrangement[p.userId] + ); + const updatedSignals = participantsWithVideoAvailable.reduce( + (acc: { [key: string]: VisibleTogetherModeSignalingAction }, p: VideoGalleryLocalParticipant) => { + const { userId, reaction, raisedHand, spotlight, isMuted, displayName } = p; + const seatingPosition = participantsSeatingArrangement[userId]; + if (seatingPosition) { + acc[userId] = { + id: userId, + reaction, + isHandRaised: !!raisedHand, + isSpotlighted: !!spotlight, + isMuted, + displayName: displayName || locale.strings.videoGallery.displayNamePlaceholder, + showDisplayName: !!(spotlight || raisedHand || reaction || hoveredParticipantID === userId), + seatPositionStyle: getTogetherModeSeatPositionStyle(seatingPosition) + }; } - })); - }, - [hoveredParticipantID, locale.strings.videoGallery.displayNamePlaceholder, participantsSeatingArrangement] - ); + return acc; + }, + {} + ); - useEffect(() => { - remoteParticipants?.forEach((participant) => { - if (participant.videoStream?.isAvailable) { - updateTogetherModeSeatingUI(participant); - } + const participantsNotInTogetherModeStream = Object.keys(visibleSignals).filter((id) => !updatedSignals[id]); + + setVisibleSignals((prevSignals) => { + const newSignals = { ...prevSignals, ...updatedSignals }; + participantsNotInTogetherModeStream.forEach((id) => { + delete newSignals[id]; + }); + + const hasChanges = Object.keys(newSignals).some( + (key) => JSON.stringify(newSignals[key]) !== JSON.stringify(prevSignals[key]) + ); + + return hasChanges ? newSignals : prevSignals; }); + }, [ + remoteParticipants, + localParticipant, + visibleSignals, + participantsSeatingArrangement, + locale.strings.videoGallery.displayNamePlaceholder, + hoveredParticipantID + ]); - if (localParticipant) { - if (localParticipant.videoStream?.isAvailable) { - updateTogetherModeSeatingUI(localParticipant); - } - } - }, [remoteParticipants, localParticipant, participantsSeatingArrangement, updateTogetherModeSeatingUI]); + // Trigger updates on dependency changes + useEffect(() => { + Testing(); + hideSignalForParticipantsNotInTogetherMode(); + }, [ + remoteParticipants, + localParticipant, + participantsSeatingArrangement, + hoveredParticipantID, + Testing, + hideSignalForParticipantsNotInTogetherMode + ]); return (
{Object.values(visibleSignals).map((participantSignal) => ( @@ -148,37 +182,36 @@ export const TogetherModeOverlay = React.memo( key={participantSignal.id} style={{ ...getTogetherModeParticipantOverlayStyle(participantSignal.seatPositionStyle), - border: '1px solid red', position: 'absolute', left: `${participantSignal.seatPositionStyle.seatCoordinates.left}px`, - top: `${participantSignal.seatPositionStyle.seatCoordinates.top}px` - }} - onMouseEnter={() => { - setHoveredParticipantID(`${participantSignal.id}`); - }} - onMouseLeave={() => { - setHoveredParticipantID(''); + top: `${participantSignal.seatPositionStyle.seatCoordinates.top}px`, + border: '1px solid red' }} + onMouseEnter={() => setHoveredParticipantID(participantSignal.id)} + onMouseLeave={() => setHoveredParticipantID('')} >
- {participantSignal?.reaction?.reactionType && ( + {participantSignal.reaction?.reactionType && (
)} -
- - {participantSignal.isHandRaised && ( - - - - )} - {participantSignal.showDisplayName && ( - - {participantSignal.displayName} - - )} - {participantSignal.showDisplayName && participantSignal.isMuted && ( - - - - )} - {participantSignal.isSpotlighted && ( - - - - )} - -
+
+ {participantSignal.isHandRaised && ( + + + + )} + {participantSignal.showDisplayName && ( + 100 + ? 'inline-block' + : 'none' // Completely remove the element when hidden + }} + > + {participantSignal.displayName} + + )} + {participantSignal.isMuted && ( + + )} + {participantSignal.isSpotlighted && ( + + )} +
+
+ )}
))} diff --git a/packages/react-components/src/components/VideoGallery.tsx b/packages/react-components/src/components/VideoGallery.tsx index fe8d92c7ff4..be918b6a584 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -722,7 +722,6 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { ); const screenShareParticipant = remoteParticipants.find((participant) => participant.screenShareStream?.isAvailable); - const localScreenShareStreamComponent = ( { isTogetherModeActive, onCreateTogetherModeStreamView, onStartTogetherMode, + onDisposeTogetherModeStreamView, onSetTogetherModeSceneSize, togetherModeStreams, togetherModeSeatingCoordinates, - onDisposeTogetherModeStreamView, localParticipant, remoteParticipants, reactionResources, diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx index 17a4c6ac3ba..38af570d2b3 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx @@ -114,6 +114,7 @@ export const TogetherModeLayout = (props: LayoutProps): JSX.Element => { parentWidth ]); + console.log(`CHUK == version === 12:17:9`); return screenShareComponent ? ( diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx index 434a091d6ba..89766f5c77e 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx @@ -2,7 +2,7 @@ // Licensed under the MIT License. /* @conditional-compile-remove(together-mode) */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo } from 'react'; /* @conditional-compile-remove(together-mode) */ import { _formatString, _pxToRem } from '@internal/acs-ui-common'; /* @conditional-compile-remove(together-mode) */ @@ -21,6 +21,7 @@ import { StreamMedia } from '../StreamMedia'; import { MeetingReactionOverlay } from '../MeetingReactionOverlay'; /* @conditional-compile-remove(together-mode) */ import { Stack } from '@fluentui/react'; +import { togetherModeRootStyle } from '../styles/TogetherMode.styles'; /* @conditional-compile-remove(together-mode) */ // import { togetherModeRootStyle } from '../styles/TogetherMode.styles'; // Ensure this is an object, not a string @@ -53,12 +54,19 @@ export const TogetherModeStream = React.memo( onCreateTogetherModeStreamView, onStartTogetherMode, onSetTogetherModeSceneSize, + onDisposeTogetherModeStreamView, togetherModeStreams, containerWidth, containerHeight } = props; - const [sceneDimensions, setSceneDimensions] = useState<{ width: number; height: number }>({ width: 0, height: 0 }); + useEffect(() => { + return () => { + // TODO: Isolate disposing behaviors for screenShare and videoStream + onDisposeTogetherModeStreamView && onDisposeTogetherModeStreamView(); + }; + }, [onDisposeTogetherModeStreamView]); + // Trigger startTogetherMode only when needed useEffect(() => { if (startTogetherModeEnabled && !isTogetherModeActive) { @@ -74,61 +82,54 @@ export const TogetherModeStream = React.memo( }, [togetherModeStreams?.mainVideoStream?.renderElement, onCreateTogetherModeStreamView]); // Update scene size only when container dimensions change - useEffect(() => { + const reCalculateSeatingPosition = useMemo(() => { if (onSetTogetherModeSceneSize && containerWidth && containerHeight) { - if (containerWidth !== sceneDimensions.width || containerHeight !== sceneDimensions.height) { - onSetTogetherModeSceneSize(containerWidth, containerHeight); - setSceneDimensions({ width: containerWidth, height: containerHeight }); - } + onSetTogetherModeSceneSize(containerWidth, containerHeight); } - }, [onSetTogetherModeSceneSize, containerWidth, containerHeight, sceneDimensions]); + }, [onSetTogetherModeSceneSize, containerWidth, containerHeight]); - // Memoized layout computation - const layout = getTogetherModeMainVideoLayout(props); + useEffect(() => { + // Re-render MeetingReactionOverlay on participant changes + }, [ + props.localParticipant, + props.remoteParticipants, + props.reactionResources, + props.seatingCoordinates, + containerWidth, + containerHeight, + reCalculateSeatingPosition + ]); - return layout; + const stream = props.togetherModeStreams?.mainVideoStream; + const showLoadingIndicator = !(stream && stream.isAvailable && stream.isReceiving); + // console.log(`Chuk Seating arrangement TogetherMode === ${JSON.stringify(props.seatingCoordinates)}`); + return containerWidth && containerHeight ? ( + +
+ + {props.reactionResources && ( + + )} +
+
+ ) : null; } ); - -/* @conditional-compile-remove(together-mode) */ -const getTogetherModeMainVideoLayout = (props: { - togetherModeStreams?: VideoGalleryTogetherModeStreams; - containerWidth?: number; - containerHeight?: number; - reactionResources?: ReactionResources; - localParticipant?: VideoGalleryLocalParticipant; - remoteParticipants?: VideoGalleryRemoteParticipant[]; - seatingCoordinates?: VideoGalleryTogetherModeParticipantPosition; -}): JSX.Element | null => { - const stream = props.togetherModeStreams?.mainVideoStream; - const showLoadingIndicator = stream && stream.isAvailable && stream.isReceiving; - - return props.containerWidth && props.containerHeight ? ( - -
- - {props.reactionResources && ( - - )} -
-
- ) : null; -}; diff --git a/packages/react-components/src/components/styles/TogetherMode.styles.ts b/packages/react-components/src/components/styles/TogetherMode.styles.ts index d0a2d47715b..d519c16139f 100644 --- a/packages/react-components/src/components/styles/TogetherMode.styles.ts +++ b/packages/react-components/src/components/styles/TogetherMode.styles.ts @@ -6,8 +6,6 @@ import { _pxToRem } from '@internal/acs-ui-common'; import { BaseCustomStyles } from '../../types/CustomStylesProps'; /* @conditional-compile-remove(together-mode) */ import { VideoGalleryTogetherModeSeatingInfo } from '../../types/TogetherModeTypes'; -import { mergeStyles } from '@fluentui/react'; - /* @conditional-compile-remove(together-mode) */ /** * Interface for defining the coordinates of a seat in Together Mode. @@ -38,7 +36,7 @@ export interface ITogetherModeSeatPositionStyle { * @param height - The height of the root element. * @returns The base custom styles for the root element. */ -export const togetherModeRootStyle = (width: number, height: number): BaseCustomStyles => { +export const togetherModeRootStyle = (): BaseCustomStyles => { /** * The root style for Together Mode. */ @@ -98,38 +96,128 @@ export function getTogetherModeParticipantOverlayStyle( seatingPositionStyle: ITogetherModeSeatPositionStyle ): React.CSSProperties { return { - border: '1px solid green', - ...seatingPositionStyle.seatCoordinates, - zIndex: 20 + // border: '1px solid green', + ...seatingPositionStyle.seatCoordinates }; } +// Function to map a value from one range to another +const mapRange = (value: number, inMin: number, inMax: number, outMin: number, outMax: number): number => { + return outMin + ((value - inMin) * (outMax - outMin)) / (inMax - inMin); +}; + /** - * Generates the hover style for Together Mode. + * Calculate the scaled size based on width and height. * - * @returns The style object for the hover state. + * @param width - The width of the element. + * @param height - The height of the element. + * @returns The scaled size. */ -export function togetherModeHover(): React.CSSProperties { - return { - border: '1px solid blue' - }; -} +export const calculateScaledSize = (width: number, height: number): number => { + const maxSize = 600; + const minSize = 200; + const minScaledSize = 35; + const maxScaledSize = 70; + // Use width or height to determine scaling factor + const size = Math.min(width, height); + + // Map the size to the desired range + return mapRange(size, minSize, maxSize, minScaledSize, maxScaledSize); +}; + +/////// +import { CSSProperties } from 'react'; +import { moveAnimationStyles } from './ReactionOverlay.style'; /** - * Style for text with ellipsis overflow. + * General container style. */ -export const ellipsisTextStyle = mergeStyles({ - whiteSpace: 'nowrap', - overflow: 'hidden', - width: '70%', - textOverflow: 'ellipsis', - display: 'inline-block', - transition: 'width 0.3s ease-in-out, transform 0.3s ease-in-out', // Smooth transition for all changes +export const containerStyle: CSSProperties = { + width: '100%', + height: '100%', + position: 'absolute', + top: 0, + left: 0 +}; - selectors: { - '&:hover': { - width: 'auto' /* Allow the container to expand */, - transform: 'translateX(0)' // Reset any movement when hovered - } - } +/** + * Generates the style for the participant signal container. + * + * @param seatCoordinates - The coordinates of the seat. + * @returns The style object for the participant signal container. + */ +export const participantSignalStyle = (seatCoordinates: { left: number; top: number }): CSSProperties => ({ + position: 'absolute', + left: `${seatCoordinates.left}px`, + top: `${seatCoordinates.top}px`, + border: '1px solid red' +}); + +/** + * Generates the style for the move animation container. + * + * @param height - The height of the container. + * @param offset - The offset for the animation. + * @returns The style object for the move animation container. + */ +export const moveAnimationContainerStyle = (height: number, offset: number): CSSProperties => ({ + ...moveAnimationStyles(height * 0.5, height * 0.35) // Assuming moveAnimationStyles is imported +}); + +/** + * Generates the style for the emoji container. + * + * @param emojiSize - The size of the emoji. + * @param seatWidth - The width of the seat. + * @returns The style object for the emoji container. + */ +export const emojiContainerStyle = (emojiSize: number, seatWidth: number): CSSProperties => ({ + width: `${emojiSize}px`, + position: 'absolute', + left: `${(100 - (emojiSize / seatWidth) * 100) / 2}%` +}); + +/** + * Style for the display name container. + */ +export const displayNameContainerStyle: CSSProperties = { + position: 'absolute', + bottom: '10px', + width: '100%', + color: 'white', + textAlign: 'center' +}; + +/** + * Background container style for display name. + */ +export const displayNameBackgroundStyle: CSSProperties = { + backgroundColor: 'rgba(50, 50, 50, 1)', // Darker and greyish background + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + gap: '10px', + margin: '0 auto', // Centers the container + maxWidth: 'max-content', // Allows container to grow with content + transition: 'width 0.3s ease, max-width 0.3s ease', // Smooth transition for container expansion + padding: '0 5px', + borderRadius: '2px' +}; + +// Display name text style +/** + * Generates the style for the display name text. + * + * @param hoveredParticipantID - The ID of the participant being hovered over. + * @param participantSignalID - The ID of the participant signal. + * @returns The style object for the display name text. + */ +export const displayNameTextStyle = (hoveredParticipantID: string, participantSignalID: string): CSSProperties => ({ + textOverflow: 'ellipsis', + flexGrow: 1, // Allow text to grow within available space + overflow: hoveredParticipantID === participantSignalID ? 'visible' : 'hidden', + whiteSpace: 'nowrap', + textAlign: 'center', + width: hoveredParticipantID === `${participantSignalID}` ? 'calc(100% - 100px)' : 'auto', // Expand width from center + transition: 'width 0.3s ease' // Smooth transition for width changes }); diff --git a/packages/react-components/src/components/utils.ts b/packages/react-components/src/components/utils.ts index 70df38f7421..a6b0f7af142 100644 --- a/packages/react-components/src/components/utils.ts +++ b/packages/react-components/src/components/utils.ts @@ -359,7 +359,11 @@ export const customNotificationIconName: Partial<{ [key in NotificationType]: st /* @conditional-compile-remove(breakout-rooms) */ breakoutRoomJoined: 'NotificationBarBreakoutRoomJoined', /* @conditional-compile-remove(breakout-rooms) */ - breakoutRoomClosingSoon: 'NotificationBarBreakoutRoomClosingSoon' + breakoutRoomClosingSoon: 'NotificationBarBreakoutRoomClosingSoon', + /* @conditional-compile-remove(together-mode) */ + togetherModeStarted: 'NotificationBarTogetherModeIcon', + /* @conditional-compile-remove(together-mode) */ + togetherModeEnded: 'NotificationBarTogetherModeIcon' }; /** diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index 68ea0918467..335e4b0ae25 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -96,14 +96,5 @@ export type { SurveyIssuesHeadingStrings } from './types'; export type { CallSurveyImprovementSuggestions } from './types'; -/* @conditional-compile-remove(together-mode) */ -export type { - TogetherModeStreamViewResult, - VideoGalleryTogetherModeParticipantPosition, - VideoGalleryTogetherModeSeatingInfo, - VideoGalleryTogetherModeStreams, - TogetherModeStreamOptions -} from './types'; - /* @conditional-compile-remove(media-access) */ export type { MediaAccess } from './types'; diff --git a/packages/react-components/src/localization/locales/en-US/strings.json b/packages/react-components/src/localization/locales/en-US/strings.json index c2ef4fe89d6..4e629214ca0 100644 --- a/packages/react-components/src/localization/locales/en-US/strings.json +++ b/packages/react-components/src/localization/locales/en-US/strings.json @@ -619,12 +619,10 @@ }, "togetherModeStarted": { "title": "Togethermode has started", - "message": "Togethermode has started", "dismissButtonAriaLabel": "Close" }, "togetherModeEnded": { "title": "Togethermode has ended", - "message": "Togethermode has ended", "dismissButtonAriaLabel": "Close" } }, diff --git a/packages/react-components/src/theming/icons.tsx b/packages/react-components/src/theming/icons.tsx index 6929a9c2ba9..29def0905fa 100644 --- a/packages/react-components/src/theming/icons.tsx +++ b/packages/react-components/src/theming/icons.tsx @@ -72,6 +72,8 @@ import { VideoProhibited16Filled, WifiWarning20Filled } from '@fluentui/react-icons'; +/* @conditional-compile-remove(together-mode) */ +import { PeopleAudience20Regular } from '@fluentui/react-icons'; /* @conditional-compile-remove(rtt) */ import { SlideTextCall20Regular } from '@fluentui/react-icons'; /* @conditional-compile-remove(rich-text-editor) */ @@ -408,5 +410,7 @@ export const DEFAULT_COMPONENT_ICONS = { IncomingCallNotificationAcceptIcon: , IncomingCallNotificationAcceptWithVideoIcon: , /* @conditional-compile-remove(rtt) */ - RTTIcon: + RTTIcon: , + /* @conditional-compile-remove(together-mode) */ + NotificationBarTogetherModeIcon: }; diff --git a/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx b/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx index 121e765733a..35ecf911142 100644 --- a/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx +++ b/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx @@ -362,7 +362,7 @@ export const DesktopMoreButton = (props: DesktopMoreButtonProps): JSX.Element => isTogetherModeActive ), iconProps: { - iconName: 'LargeGalleryLayout', + iconName: 'TogetherModeLayout', styles: { root: { lineHeight: 0 } } } }; diff --git a/packages/react-composites/src/composites/common/icons.tsx b/packages/react-composites/src/composites/common/icons.tsx index cbdd05e0263..0917ff097f1 100644 --- a/packages/react-composites/src/composites/common/icons.tsx +++ b/packages/react-composites/src/composites/common/icons.tsx @@ -18,7 +18,8 @@ import { Video20Filled, VideoOff20Filled, WifiWarning20Filled, - Circle20Regular + Circle20Regular, + PeopleAudience20Regular } from '@fluentui/react-icons'; import { PersonCall20Regular, Clock20Filled } from '@fluentui/react-icons'; import { MoreHorizontal20Filled, VideoPersonStarOff20Filled } from '@fluentui/react-icons'; @@ -108,7 +109,8 @@ export const COMPOSITE_ONLY_ICONS: CompositeIcons = { JoinByPhoneConferenceIdIcon: , JoinByPhoneWaitToBeAdmittedIcon: , PeoplePaneMoreButton: , - StopAllSpotlightMenuButton: + StopAllSpotlightMenuButton: , + TogetherModeLayout: }; /** @@ -247,6 +249,7 @@ export type CallCompositeIcons = { JoinByPhoneWaitToBeAdmittedIcon?: JSX.Element; PeoplePaneMoreButton?: JSX.Element; StopAllSpotlightMenuButton?: JSX.Element; + TogetherModeLayout?: JSX.Element; }; /** From 6b58f711a686ba8dba6bc10a79f3796e873efb8c Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Fri, 20 Dec 2024 12:44:15 +0000 Subject: [PATCH 24/37] cleanup together mode styles --- packages/acs-ui-common/src/cssUtils.ts | 13 ++ packages/acs-ui-common/src/index.ts | 2 +- .../src/index-public.ts | 1 - .../src/components/MeetingReactionOverlay.tsx | 8 +- .../src/components/TogetherModeOverlay.tsx | 126 ++++------- .../VideoGallery/TogetherModeStream.tsx | 43 ++-- .../components/styles/TogetherMode.styles.ts | 213 ++++++++---------- .../src/composites/common/icons.tsx | 3 +- 8 files changed, 169 insertions(+), 240 deletions(-) diff --git a/packages/acs-ui-common/src/cssUtils.ts b/packages/acs-ui-common/src/cssUtils.ts index 7931cfc7ce2..6c043c90902 100644 --- a/packages/acs-ui-common/src/cssUtils.ts +++ b/packages/acs-ui-common/src/cssUtils.ts @@ -7,3 +7,16 @@ * For example, an input of `16` will return `1rem`. */ export const _pxToRem = (px: number): string => `${px / 16}rem`; + +/** + * @internal + * Converts rem value to px value. + * For example, an input of `1rem` will return `16`. + */ +export function _remToPx(rem: string | number, baseFontSize: number = 16): number { + // If the input is a string, strip the 'rem' suffix and convert to number + if (typeof rem === 'string') { + return parseFloat(rem) * baseFontSize; + } + return rem * baseFontSize; +} diff --git a/packages/acs-ui-common/src/index.ts b/packages/acs-ui-common/src/index.ts index 6adbafd290a..855f9d74926 100644 --- a/packages/acs-ui-common/src/index.ts +++ b/packages/acs-ui-common/src/index.ts @@ -29,7 +29,7 @@ export { _MAX_EVENT_LISTENERS } from './constants'; /* @conditional-compile-remove(rich-text-editor-image-upload) */ export { _IMAGE_ATTRIBUTE_INLINE_IMAGE_FILE_NAME_KEY } from './constants'; -export { _pxToRem } from './cssUtils'; +export { _pxToRem, _remToPx } from './cssUtils'; export { _logEvent } from './logEvent'; export type { TelemetryEvent } from './logEvent'; diff --git a/packages/calling-stateful-client/src/index-public.ts b/packages/calling-stateful-client/src/index-public.ts index 6b6b33e8dfa..e09a7345a1d 100644 --- a/packages/calling-stateful-client/src/index-public.ts +++ b/packages/calling-stateful-client/src/index-public.ts @@ -33,7 +33,6 @@ export type { CreateViewResult } from './StreamUtils'; export type { RaiseHandCallFeatureState as RaiseHandCallFeature } from './CallClientState'; /* @conditional-compile-remove(together-mode) */ export type { - TogetherModeCallFeatureState, CallFeatureStreamState, TogetherModeSeatingPositionState, CallFeatureStreamName, diff --git a/packages/react-components/src/components/MeetingReactionOverlay.tsx b/packages/react-components/src/components/MeetingReactionOverlay.tsx index 72f36953ad3..73af85e785a 100644 --- a/packages/react-components/src/components/MeetingReactionOverlay.tsx +++ b/packages/react-components/src/components/MeetingReactionOverlay.tsx @@ -46,7 +46,7 @@ export interface MeetingReactionOverlayProps { remoteParticipants?: VideoGalleryRemoteParticipant[]; /* @conditional-compile-remove(together-mode) */ - seatingCoordinates?: VideoGalleryTogetherModeParticipantPosition; + togetherModeSeatPositions?: VideoGalleryTogetherModeParticipantPosition; } /** @@ -75,7 +75,8 @@ const REACTION_EMOJI_RESIZE_SCALE_CONSTANT = 3; * @internal */ export const MeetingReactionOverlay = (props: MeetingReactionOverlayProps): JSX.Element => { - const { overlayMode, reaction, reactionResources, localParticipant, remoteParticipants, seatingCoordinates } = props; + const { overlayMode, reaction, reactionResources, localParticipant, remoteParticipants, togetherModeSeatPositions } = + props; const [emojiSizePx, setEmojiSizePx] = useState(0); const [divHeight, setDivHeight] = useState(0); const [divWidth, setDivWidth] = useState(0); @@ -136,7 +137,6 @@ export const MeetingReactionOverlay = (props: MeetingReactionOverlayProps): JSX. /* @conditional-compile-remove(together-mode) */ return (
); diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index 382332f662a..bd92c433f94 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -31,16 +31,17 @@ import { _HighContrastAwareIcon } from './HighContrastAwareIcon'; import { calculateScaledSize, getTogetherModeParticipantOverlayStyle, - getTogetherModeSeatPositionStyle, - ITogetherModeSeatPositionStyle + setTogetherModeSeatPositionStyle, + togetherModeIconStyle, + togetherModeParticipantDisplayName, + togetherModeParticipantStatusContainer, + TogetherModeSeatStyle } from './styles/TogetherMode.styles'; import { CallingTheme, useTheme } from '../theming'; // import { iconContainerStyle, raiseHandContainerStyles } from './styles/VideoTile.styles'; import { RaisedHandIcon } from './assets/RaisedHandIcon'; /* @conditional-compile-remove(together-mode) */ -// import { iconContainerStyle } from './styles/VideoTile.styles'; -// import { useTheme } from '../theming'; - +import { _pxToRem, _remToPx } from '@internal/acs-ui-common'; /* @conditional-compile-remove(together-mode) */ /** * Signaling action overlay component props @@ -49,15 +50,16 @@ import { RaisedHandIcon } from './assets/RaisedHandIcon'; * * @internal */ -type VisibleTogetherModeSignalingAction = { +type TogetherModeParticipantStatus = { reaction?: Reaction; + scaledSize?: number; isHandRaised?: boolean; isSpotlighted?: boolean; isMuted?: boolean; id: string; - seatPositionStyle: ITogetherModeSeatPositionStyle; - displayName?: string; - showDisplayName?: boolean; + seatPositionStyle: TogetherModeSeatStyle; + displayName: string; + showDisplayName: boolean; }; /* @conditional-compile-remove(together-mode) */ @@ -72,20 +74,19 @@ export const TogetherModeOverlay = React.memo( reactionResources: ReactionResources; localParticipant: VideoGalleryLocalParticipant; remoteParticipants: VideoGalleryRemoteParticipant[]; - participantsSeatingArrangement: VideoGalleryTogetherModeParticipantPosition; + togetherModeSeatPositions: VideoGalleryTogetherModeParticipantPosition; }) => { const locale = useLocale(); const theme = useTheme(); const callingPalette = (theme as unknown as CallingTheme).callingPalette; - const { emojiSize, reactionResources, remoteParticipants, localParticipant, participantsSeatingArrangement } = - props; - const [visibleSignals, setVisibleSignals] = useState<{ [key: string]: VisibleTogetherModeSignalingAction }>({}); + const { emojiSize, reactionResources, remoteParticipants, localParticipant, togetherModeSeatPositions } = props; + const [visibleSignals, setVisibleSignals] = useState<{ [key: string]: TogetherModeParticipantStatus }>({}); const [hoveredParticipantID, setHoveredParticipantID] = useState(''); const hideSignalForParticipantsNotInTogetherMode = useCallback(() => { const removedVisibleParticipants = Object.keys(visibleSignals).filter( - (participantId) => !participantsSeatingArrangement[participantId] + (participantId) => !togetherModeSeatPositions[participantId] ); // Update visible signals state instead of directly mutating it setVisibleSignals((prevSignals) => { @@ -101,18 +102,18 @@ export const TogetherModeOverlay = React.memo( } return prevSignals; }); - }, [visibleSignals, participantsSeatingArrangement]); + }, [visibleSignals, togetherModeSeatPositions]); const Testing = useCallback(() => { const allParticipants = [...remoteParticipants, localParticipant]; const participantsWithVideoAvailable = allParticipants.filter( - (p) => p.videoStream?.isAvailable && participantsSeatingArrangement[p.userId] + (p) => p.videoStream?.isAvailable && togetherModeSeatPositions[p.userId] ); const updatedSignals = participantsWithVideoAvailable.reduce( - (acc: { [key: string]: VisibleTogetherModeSignalingAction }, p: VideoGalleryLocalParticipant) => { + (acc: { [key: string]: TogetherModeParticipantStatus }, p: VideoGalleryLocalParticipant) => { const { userId, reaction, raisedHand, spotlight, isMuted, displayName } = p; - const seatingPosition = participantsSeatingArrangement[userId]; + const seatingPosition = togetherModeSeatPositions[userId]; if (seatingPosition) { acc[userId] = { id: userId, @@ -122,7 +123,8 @@ export const TogetherModeOverlay = React.memo( isMuted, displayName: displayName || locale.strings.videoGallery.displayNamePlaceholder, showDisplayName: !!(spotlight || raisedHand || reaction || hoveredParticipantID === userId), - seatPositionStyle: getTogetherModeSeatPositionStyle(seatingPosition) + scaledSize: calculateScaledSize(seatingPosition.width, seatingPosition.height), + seatPositionStyle: setTogetherModeSeatPositionStyle(seatingPosition) }; } return acc; @@ -148,7 +150,7 @@ export const TogetherModeOverlay = React.memo( remoteParticipants, localParticipant, visibleSignals, - participantsSeatingArrangement, + togetherModeSeatPositions, locale.strings.videoGallery.displayNamePlaceholder, hoveredParticipantID ]); @@ -160,58 +162,48 @@ export const TogetherModeOverlay = React.memo( }, [ remoteParticipants, localParticipant, - participantsSeatingArrangement, hoveredParticipantID, Testing, hideSignalForParticipantsNotInTogetherMode ]); return ( -
+
{Object.values(visibleSignals).map((participantSignal) => (
setHoveredParticipantID(participantSignal.id)} onMouseLeave={() => setHoveredParticipantID('')} > -
+
{participantSignal.reaction?.reactionType && (
{participantSignal.isHandRaised && ( @@ -260,19 +242,11 @@ export const TogetherModeOverlay = React.memo( {participantSignal.showDisplayName && ( 100 - ? 'inline-block' - : 'none' // Completely remove the element when hidden + ...togetherModeParticipantDisplayName( + hoveredParticipantID === participantSignal.id, + _remToPx(participantSignal.seatPositionStyle.seatPosition.width), + participantSignal.displayName ? theme.palette.neutralSecondary : 'inherit' + ) }} > {participantSignal.displayName} @@ -281,10 +255,8 @@ export const TogetherModeOverlay = React.memo( {participantSignal.isMuted && ( @@ -292,10 +264,8 @@ export const TogetherModeOverlay = React.memo( {participantSignal.isSpotlighted && ( diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx index 89766f5c77e..21ff44ffd8e 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx @@ -21,10 +21,8 @@ import { StreamMedia } from '../StreamMedia'; import { MeetingReactionOverlay } from '../MeetingReactionOverlay'; /* @conditional-compile-remove(together-mode) */ import { Stack } from '@fluentui/react'; -import { togetherModeRootStyle } from '../styles/TogetherMode.styles'; /* @conditional-compile-remove(together-mode) */ -// import { togetherModeRootStyle } from '../styles/TogetherMode.styles'; // Ensure this is an object, not a string - +import { togetherModeStreamRootStyle } from '../styles/TogetherMode.styles'; /* @conditional-compile-remove(together-mode) */ /** * A memoized version of local screen share component. React.memo is used for a performance @@ -102,33 +100,20 @@ export const TogetherModeStream = React.memo( const stream = props.togetherModeStreams?.mainVideoStream; const showLoadingIndicator = !(stream && stream.isAvailable && stream.isReceiving); - // console.log(`Chuk Seating arrangement TogetherMode === ${JSON.stringify(props.seatingCoordinates)}`); return containerWidth && containerHeight ? ( - -
- - {props.reactionResources && ( - - )} -
+ + + ) : null; } diff --git a/packages/react-components/src/components/styles/TogetherMode.styles.ts b/packages/react-components/src/components/styles/TogetherMode.styles.ts index d519c16139f..43ba17a6f91 100644 --- a/packages/react-components/src/components/styles/TogetherMode.styles.ts +++ b/packages/react-components/src/components/styles/TogetherMode.styles.ts @@ -3,52 +3,30 @@ /* @conditional-compile-remove(together-mode) */ import { _pxToRem } from '@internal/acs-ui-common'; /* @conditional-compile-remove(together-mode) */ -import { BaseCustomStyles } from '../../types/CustomStylesProps'; -/* @conditional-compile-remove(together-mode) */ import { VideoGalleryTogetherModeSeatingInfo } from '../../types/TogetherModeTypes'; +/* @conditional-compile-remove(together-mode) */ +import { IStackStyles } from '@fluentui/react'; +import React from 'react'; + /* @conditional-compile-remove(together-mode) */ /** * Interface for defining the coordinates of a seat in Together Mode. */ -export interface ITogetherModeSeatCoordinates { - height?: number; - width?: number; - left?: number; - top?: number; +export interface TogetherModeParticipantSeatPosition { + height: string; + width: string; + left: string; + top: string; } /* @conditional-compile-remove(together-mode) */ /** * Interface for defining the style of a seat position in Together Mode. */ -export interface ITogetherModeSeatPositionStyle { - sizeScale: number; - opacityMax: number; - seatCoordinates: ITogetherModeSeatCoordinates; - position?: string; +export interface TogetherModeSeatStyle { + seatPosition: TogetherModeParticipantSeatPosition; } -/* @conditional-compile-remove(together-mode) */ -/** - * Generates the root style for Together Mode. - * - * @param width - The width of the root element. - * @param height - The height of the root element. - * @returns The base custom styles for the root element. - */ -export const togetherModeRootStyle = (): BaseCustomStyles => { - /** - * The root style for Together Mode. - */ - const rootStyle = { - root: { - width: `100%`, - height: `100%` - } - }; - return rootStyle; -}; - /* @conditional-compile-remove(together-mode) */ /** * Sets the seating position for a participant in Together Mode. @@ -58,12 +36,12 @@ export const togetherModeRootStyle = (): BaseCustomStyles => { */ export function setParticipantSeatingPosition( seatingPosition: VideoGalleryTogetherModeSeatingInfo -): ITogetherModeSeatCoordinates { +): TogetherModeParticipantSeatPosition { return { - width: seatingPosition.width || 0, - height: seatingPosition.height || 0, - left: seatingPosition.left || 0, - top: seatingPosition.top || 0 + width: _pxToRem(seatingPosition.width), + height: _pxToRem(seatingPosition.height), + left: _pxToRem(seatingPosition.left), + top: _pxToRem(seatingPosition.top) }; } @@ -74,14 +52,11 @@ export function setParticipantSeatingPosition( * height, width, and opacity. * @private */ -export function getTogetherModeSeatPositionStyle( +export function setTogetherModeSeatPositionStyle( seatingPosition: VideoGalleryTogetherModeSeatingInfo -): ITogetherModeSeatPositionStyle { +): TogetherModeSeatStyle { return { - sizeScale: 0.9, - opacityMax: 0.9, - seatCoordinates: setParticipantSeatingPosition(seatingPosition), - position: 'absolute' + seatPosition: setParticipantSeatingPosition(seatingPosition) }; } @@ -93,11 +68,11 @@ export function getTogetherModeSeatPositionStyle( * @returns The style object for the participant overlay. */ export function getTogetherModeParticipantOverlayStyle( - seatingPositionStyle: ITogetherModeSeatPositionStyle + seatingPositionStyle: TogetherModeSeatStyle ): React.CSSProperties { return { - // border: '1px solid green', - ...seatingPositionStyle.seatCoordinates + ...seatingPositionStyle.seatPosition, + position: 'absolute' }; } @@ -126,98 +101,86 @@ export const calculateScaledSize = (width: number, height: number): number => { return mapRange(size, minSize, maxSize, minScaledSize, maxScaledSize); }; -/////// -import { CSSProperties } from 'react'; -import { moveAnimationStyles } from './ReactionOverlay.style'; -/** - * General container style. - */ -export const containerStyle: CSSProperties = { - width: '100%', - height: '100%', - position: 'absolute', - top: 0, - left: 0 -}; - /** - * Generates the style for the participant signal container. + * Get the root participant status div style. * - * @param seatCoordinates - The coordinates of the seat. - * @returns The style object for the participant signal container. + * @returns The style object for the root participant status div. */ -export const participantSignalStyle = (seatCoordinates: { left: number; top: number }): CSSProperties => ({ - position: 'absolute', - left: `${seatCoordinates.left}px`, - top: `${seatCoordinates.top}px`, - border: '1px solid red' -}); - -/** - * Generates the style for the move animation container. - * - * @param height - The height of the container. - * @param offset - The offset for the animation. - * @returns The style object for the move animation container. - */ -export const moveAnimationContainerStyle = (height: number, offset: number): CSSProperties => ({ - ...moveAnimationStyles(height * 0.5, height * 0.35) // Assuming moveAnimationStyles is imported -}); +export const getRootParticipantStatusDivStatus = (): React.CSSProperties => { + return { + width: '100%', + height: '100%', + position: 'absolute', + top: 0, + left: 0 + }; +}; +/* @conditional-compile-remove(together-mode) */ /** - * Generates the style for the emoji container. - * - * @param emojiSize - The size of the emoji. - * @param seatWidth - The width of the seat. - * @returns The style object for the emoji container. + * @private */ -export const emojiContainerStyle = (emojiSize: number, seatWidth: number): CSSProperties => ({ - width: `${emojiSize}px`, - position: 'absolute', - left: `${(100 - (emojiSize / seatWidth) * 100) / 2}%` -}); +export const togetherModeStreamRootStyle: IStackStyles = { + root: { + width: '100%', + height: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + position: 'absolute', // Prevents sliding due to resizing + top: 0, // Anchors the div at the top + left: 0 // Anchors the div at the left + } +}; +/* @conditional-compile-remove(together-mode) */ /** - * Style for the display name container. + * @private */ -export const displayNameContainerStyle: CSSProperties = { - position: 'absolute', - bottom: '10px', - width: '100%', - color: 'white', - textAlign: 'center' +export const togetherModeIconStyle = (): React.CSSProperties => { + return { + width: '20px', + flexShrink: 0 + }; }; +/* @conditional-compile-remove(together-mode) */ /** - * Background container style for display name. + * @private */ -export const displayNameBackgroundStyle: CSSProperties = { - backgroundColor: 'rgba(50, 50, 50, 1)', // Darker and greyish background - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - gap: '10px', - margin: '0 auto', // Centers the container - maxWidth: 'max-content', // Allows container to grow with content - transition: 'width 0.3s ease, max-width 0.3s ease', // Smooth transition for container expansion - padding: '0 5px', - borderRadius: '2px' +export const togetherModeParticipantStatusContainer = ( + backgroundColor: string, + borderRadius: string +): React.CSSProperties => { + return { + backgroundColor, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + gap: '2px', + margin: '0 auto', // Centers the container + padding: '0 5px', + borderRadius, + width: 'fit-content' + }; }; -// Display name text style +/* @conditional-compile-remove(together-mode) */ /** - * Generates the style for the display name text. - * - * @param hoveredParticipantID - The ID of the participant being hovered over. - * @param participantSignalID - The ID of the participant signal. - * @returns The style object for the display name text. + * @private */ -export const displayNameTextStyle = (hoveredParticipantID: string, participantSignalID: string): CSSProperties => ({ - textOverflow: 'ellipsis', - flexGrow: 1, // Allow text to grow within available space - overflow: hoveredParticipantID === participantSignalID ? 'visible' : 'hidden', - whiteSpace: 'nowrap', - textAlign: 'center', - width: hoveredParticipantID === `${participantSignalID}` ? 'calc(100% - 100px)' : 'auto', // Expand width from center - transition: 'width 0.3s ease' // Smooth transition for width changes -}); +export const togetherModeParticipantDisplayName = ( + isParticipantHovered: boolean, + participantSeatingWidth: number, + color: string +): React.CSSProperties => { + return { + textOverflow: 'ellipsis', + flexGrow: 1, // Allow text to grow within available space + overflow: isParticipantHovered ? 'visible' : 'hidden', + whiteSpace: 'nowrap', + textAlign: 'center', + color, + display: isParticipantHovered || participantSeatingWidth > 100 ? 'inline-block' : 'none' // Completely remove the element when hidden + }; +}; diff --git a/packages/react-composites/src/composites/common/icons.tsx b/packages/react-composites/src/composites/common/icons.tsx index 25d2e75e074..f9f056a6d1e 100644 --- a/packages/react-composites/src/composites/common/icons.tsx +++ b/packages/react-composites/src/composites/common/icons.tsx @@ -18,8 +18,7 @@ import { Video20Filled, VideoOff20Filled, WifiWarning20Filled, - Circle20Regular, - PeopleAudience20Regular + Circle20Regular } from '@fluentui/react-icons'; import { PersonCall20Regular, Clock20Filled } from '@fluentui/react-icons'; import { MoreHorizontal20Filled, VideoPersonStarOff20Filled } from '@fluentui/react-icons'; From 136e0618232c3ba0983b0b318478b41d33bf0892 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Sat, 21 Dec 2024 07:50:30 +0000 Subject: [PATCH 25/37] Addressed comments --- packages/acs-ui-common/src/cssUtils.ts | 13 --- packages/acs-ui-common/src/index.ts | 2 +- .../src/components/TogetherModeOverlay.tsx | 86 +++++++++---------- .../VideoGallery/TogetherModeStream.tsx | 14 +-- .../components/styles/TogetherMode.styles.ts | 6 +- 5 files changed, 44 insertions(+), 77 deletions(-) diff --git a/packages/acs-ui-common/src/cssUtils.ts b/packages/acs-ui-common/src/cssUtils.ts index 6c043c90902..7931cfc7ce2 100644 --- a/packages/acs-ui-common/src/cssUtils.ts +++ b/packages/acs-ui-common/src/cssUtils.ts @@ -7,16 +7,3 @@ * For example, an input of `16` will return `1rem`. */ export const _pxToRem = (px: number): string => `${px / 16}rem`; - -/** - * @internal - * Converts rem value to px value. - * For example, an input of `1rem` will return `16`. - */ -export function _remToPx(rem: string | number, baseFontSize: number = 16): number { - // If the input is a string, strip the 'rem' suffix and convert to number - if (typeof rem === 'string') { - return parseFloat(rem) * baseFontSize; - } - return rem * baseFontSize; -} diff --git a/packages/acs-ui-common/src/index.ts b/packages/acs-ui-common/src/index.ts index 855f9d74926..6adbafd290a 100644 --- a/packages/acs-ui-common/src/index.ts +++ b/packages/acs-ui-common/src/index.ts @@ -29,7 +29,7 @@ export { _MAX_EVENT_LISTENERS } from './constants'; /* @conditional-compile-remove(rich-text-editor-image-upload) */ export { _IMAGE_ATTRIBUTE_INLINE_IMAGE_FILE_NAME_KEY } from './constants'; -export { _pxToRem, _remToPx } from './cssUtils'; +export { _pxToRem } from './cssUtils'; export { _logEvent } from './logEvent'; export type { TelemetryEvent } from './logEvent'; diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index bd92c433f94..bf7e4faef18 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -2,7 +2,7 @@ // Licensed under the MIT License. /* @conditional-compile-remove(together-mode) */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useMemo, useState } from 'react'; /* @conditional-compile-remove(together-mode) */ import { Reaction, @@ -41,7 +41,7 @@ import { CallingTheme, useTheme } from '../theming'; // import { iconContainerStyle, raiseHandContainerStyles } from './styles/VideoTile.styles'; import { RaisedHandIcon } from './assets/RaisedHandIcon'; /* @conditional-compile-remove(together-mode) */ -import { _pxToRem, _remToPx } from '@internal/acs-ui-common'; +import { _pxToRem } from '@internal/acs-ui-common'; /* @conditional-compile-remove(together-mode) */ /** * Signaling action overlay component props @@ -81,15 +81,17 @@ export const TogetherModeOverlay = React.memo( const callingPalette = (theme as unknown as CallingTheme).callingPalette; const { emojiSize, reactionResources, remoteParticipants, localParticipant, togetherModeSeatPositions } = props; - const [visibleSignals, setVisibleSignals] = useState<{ [key: string]: TogetherModeParticipantStatus }>({}); + const [togetherModeParticipantStatus, setTogetherModeParticipantStatus] = useState<{ + [key: string]: TogetherModeParticipantStatus; + }>({}); const [hoveredParticipantID, setHoveredParticipantID] = useState(''); - const hideSignalForParticipantsNotInTogetherMode = useCallback(() => { - const removedVisibleParticipants = Object.keys(visibleSignals).filter( + useMemo(() => { + const removedVisibleParticipants = Object.keys(togetherModeParticipantStatus).filter( (participantId) => !togetherModeSeatPositions[participantId] ); // Update visible signals state instead of directly mutating it - setVisibleSignals((prevSignals) => { + setTogetherModeParticipantStatus((prevSignals) => { const newSignals = { ...prevSignals }; removedVisibleParticipants.forEach((participantId) => { delete newSignals[participantId]; @@ -102,9 +104,9 @@ export const TogetherModeOverlay = React.memo( } return prevSignals; }); - }, [visibleSignals, togetherModeSeatPositions]); + }, [togetherModeParticipantStatus, togetherModeSeatPositions]); - const Testing = useCallback(() => { + useMemo(() => { const allParticipants = [...remoteParticipants, localParticipant]; const participantsWithVideoAvailable = allParticipants.filter( @@ -132,9 +134,11 @@ export const TogetherModeOverlay = React.memo( {} ); - const participantsNotInTogetherModeStream = Object.keys(visibleSignals).filter((id) => !updatedSignals[id]); + const participantsNotInTogetherModeStream = Object.keys(togetherModeParticipantStatus).filter( + (id) => !togetherModeParticipantStatus[id] + ); - setVisibleSignals((prevSignals) => { + setTogetherModeParticipantStatus((prevSignals) => { const newSignals = { ...prevSignals, ...updatedSignals }; participantsNotInTogetherModeStream.forEach((id) => { delete newSignals[id]; @@ -149,42 +153,30 @@ export const TogetherModeOverlay = React.memo( }, [ remoteParticipants, localParticipant, - visibleSignals, + togetherModeParticipantStatus, togetherModeSeatPositions, locale.strings.videoGallery.displayNamePlaceholder, hoveredParticipantID ]); - // Trigger updates on dependency changes - useEffect(() => { - Testing(); - hideSignalForParticipantsNotInTogetherMode(); - }, [ - remoteParticipants, - localParticipant, - hoveredParticipantID, - Testing, - hideSignalForParticipantsNotInTogetherMode - ]); - return (
- {Object.values(visibleSignals).map((participantSignal) => ( + {Object.values(togetherModeParticipantStatus).map((participantStatus) => (
setHoveredParticipantID(participantSignal.id)} + onMouseEnter={() => setHoveredParticipantID(participantStatus.id)} onMouseLeave={() => setHoveredParticipantID('')} >
- {participantSignal.reaction?.reactionType && ( + {participantStatus.reaction?.reactionType && (
@@ -213,7 +205,7 @@ export const TogetherModeOverlay = React.memo(
)} - {participantSignal.showDisplayName && ( + {participantStatus.showDisplayName && (
- {participantSignal.isHandRaised && ( + {participantStatus.isHandRaised && ( )} - {participantSignal.showDisplayName && ( + {participantStatus.showDisplayName && ( - {participantSignal.displayName} + {participantStatus.displayName} )} - {participantSignal.isMuted && ( + {participantStatus.isMuted && ( )} - {participantSignal.isSpotlighted && ( + {participantStatus.isSpotlighted && ( )} diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx index 21ff44ffd8e..19f1c6645ad 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx @@ -80,24 +80,12 @@ export const TogetherModeStream = React.memo( }, [togetherModeStreams?.mainVideoStream?.renderElement, onCreateTogetherModeStreamView]); // Update scene size only when container dimensions change - const reCalculateSeatingPosition = useMemo(() => { + useMemo(() => { if (onSetTogetherModeSceneSize && containerWidth && containerHeight) { onSetTogetherModeSceneSize(containerWidth, containerHeight); } }, [onSetTogetherModeSceneSize, containerWidth, containerHeight]); - useEffect(() => { - // Re-render MeetingReactionOverlay on participant changes - }, [ - props.localParticipant, - props.remoteParticipants, - props.reactionResources, - props.seatingCoordinates, - containerWidth, - containerHeight, - reCalculateSeatingPosition - ]); - const stream = props.togetherModeStreams?.mainVideoStream; const showLoadingIndicator = !(stream && stream.isAvailable && stream.isReceiving); return containerWidth && containerHeight ? ( diff --git a/packages/react-components/src/components/styles/TogetherMode.styles.ts b/packages/react-components/src/components/styles/TogetherMode.styles.ts index 43ba17a6f91..c8898267e6d 100644 --- a/packages/react-components/src/components/styles/TogetherMode.styles.ts +++ b/packages/react-components/src/components/styles/TogetherMode.styles.ts @@ -127,9 +127,9 @@ export const togetherModeStreamRootStyle: IStackStyles = { display: 'flex', justifyContent: 'center', alignItems: 'center', - position: 'absolute', // Prevents sliding due to resizing - top: 0, // Anchors the div at the top - left: 0 // Anchors the div at the left + position: 'absolute', + top: 0, + left: 0 } }; From 4bd0aa28f13c3735cfb5e36e072d14667526ff3d Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Thu, 26 Dec 2024 00:52:05 +0000 Subject: [PATCH 26/37] Included mobile menu --- .../src/components/TogetherModeOverlay.tsx | 60 +++++++++++-------- .../VideoGallery/TogetherModeLayout.tsx | 2 +- .../VideoGallery/TogetherModeStream.tsx | 5 ++ .../components/styles/TogetherMode.styles.ts | 43 +++++++------ .../composites/common/Drawer/MoreDrawer.tsx | 29 +++++++++ 5 files changed, 95 insertions(+), 44 deletions(-) diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index bf7e4faef18..21d0983872b 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -119,12 +119,12 @@ export const TogetherModeOverlay = React.memo( if (seatingPosition) { acc[userId] = { id: userId, - reaction, + reaction: reactionResources && reaction, isHandRaised: !!raisedHand, isSpotlighted: !!spotlight, isMuted, displayName: displayName || locale.strings.videoGallery.displayNamePlaceholder, - showDisplayName: !!(spotlight || raisedHand || reaction || hoveredParticipantID === userId), + showDisplayName: !!(spotlight || raisedHand || hoveredParticipantID === userId), scaledSize: calculateScaledSize(seatingPosition.width, seatingPosition.height), seatPositionStyle: setTogetherModeSeatPositionStyle(seatingPosition) }; @@ -135,17 +135,20 @@ export const TogetherModeOverlay = React.memo( ); const participantsNotInTogetherModeStream = Object.keys(togetherModeParticipantStatus).filter( - (id) => !togetherModeParticipantStatus[id] + (id) => !updatedSignals[id] ); setTogetherModeParticipantStatus((prevSignals) => { const newSignals = { ...prevSignals, ...updatedSignals }; + const newSignalsLength = Object.keys(newSignals).length; participantsNotInTogetherModeStream.forEach((id) => { delete newSignals[id]; }); const hasChanges = Object.keys(newSignals).some( - (key) => JSON.stringify(newSignals[key]) !== JSON.stringify(prevSignals[key]) + (key) => + JSON.stringify(newSignals[key]) !== JSON.stringify(prevSignals[key]) || + newSignalsLength !== Object.keys(prevSignals).length ); return hasChanges ? newSignals : prevSignals; @@ -155,6 +158,7 @@ export const TogetherModeOverlay = React.memo( localParticipant, togetherModeParticipantStatus, togetherModeSeatPositions, + reactionResources, locale.strings.videoGallery.displayNamePlaceholder, hoveredParticipantID ]); @@ -166,7 +170,7 @@ export const TogetherModeOverlay = React.memo( key={participantStatus.id} style={{ ...getTogetherModeParticipantOverlayStyle(participantStatus.seatPositionStyle), - border: '1px solid yellow' + border: '1px solid blue' }} onMouseEnter={() => setHoveredParticipantID(participantStatus.id)} onMouseLeave={() => setHoveredParticipantID('')} @@ -175,8 +179,8 @@ export const TogetherModeOverlay = React.memo( {participantStatus.reaction?.reactionType && (
)} {participantStatus.isMuted && ( - + + + )} {participantStatus.isSpotlighted && ( - + + + )}
diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx index 38af570d2b3..7a2be79e46d 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx @@ -114,7 +114,7 @@ export const TogetherModeLayout = (props: LayoutProps): JSX.Element => { parentWidth ]); - console.log(`CHUK == version === 12:17:9`); + console.log(`CHUK == version === mobile fix`); return screenShareComponent ? ( diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx index 19f1c6645ad..b828c0d2619 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx @@ -94,6 +94,11 @@ export const TogetherModeStream = React.memo( videoStreamElement={stream?.renderElement || null} isMirrored={true} loadingState={showLoadingIndicator ? 'loading' : 'none'} + styles={{ + root: { + border: '2px solid yellow' + } + }} /> { - return { - width: '20px', - flexShrink: 0 - }; -}; - /* @conditional-compile-remove(together-mode) */ /** * @private @@ -157,11 +147,10 @@ export const togetherModeParticipantStatusContainer = ( display: 'flex', justifyContent: 'center', alignItems: 'center', - gap: '2px', + gap: `${_pxToRem(5)}`, margin: '0 auto', // Centers the container - padding: '0 5px', - borderRadius, - width: 'fit-content' + padding: `0 ${_pxToRem(5)}`, + borderRadius }; }; @@ -174,13 +163,29 @@ export const togetherModeParticipantDisplayName = ( participantSeatingWidth: number, color: string ): React.CSSProperties => { + console.log(`CHUK ---Paarticipant width == ${participantSeatingWidth}`); return { textOverflow: 'ellipsis', - flexGrow: 1, // Allow text to grow within available space - overflow: isParticipantHovered ? 'visible' : 'hidden', whiteSpace: 'nowrap', textAlign: 'center', color, - display: isParticipantHovered || participantSeatingWidth > 100 ? 'inline-block' : 'none' // Completely remove the element when hidden + overflow: isParticipantHovered ? 'visible' : 'hidden', + width: isParticipantHovered ? `100%` : _pxToRem(0.7 * participantSeatingWidth * 16), + // border: '1px solid red', + display: isParticipantHovered || participantSeatingWidth * 16 > 150 ? 'inline-block' : 'none', // Completely remove the element when hidden + fontSize: `${_pxToRem(13)}`, + lineHeight: `${_pxToRem(20)}` + }; +}; + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +export const togetherModeIconStyle = (): React.CSSProperties => { + return { + width: 'fit-content', + display: 'flex', + alignItems: 'center' }; }; diff --git a/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx b/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx index f402cfce7ae..26e8315447a 100644 --- a/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx +++ b/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx @@ -43,6 +43,8 @@ import { showDtmfDialer } from '../../CallComposite/utils/MediaGalleryUtils'; import { SpokenLanguageSettingsDrawer } from './SpokenLanguageSettingsDrawer'; import { DtmfDialPadOptions } from '../../CallComposite'; import { getRemoteParticipantsConnectedSelector } from '../../CallComposite/selectors/mediaGallerySelector'; +/* @conditional-compile-remove(together-mode) */ +import { getCapabilites, getIsTogetherModeActive, getLocalUserId } from '../../CallComposite/selectors/baseSelectors'; /** @private */ export interface MoreDrawerStrings { @@ -185,6 +187,12 @@ export const MoreDrawer = (props: MoreDrawerProps): JSX.Element => { const [dtmfDialerChecked, setDtmfDialerChecked] = useState(props.dtmfDialerPresent ?? false); const raiseHandButtonProps = usePropsFor(RaiseHandButton) as RaiseHandButtonProps; + /* @conditional-compile-remove(together-mode) */ + const participantCapability = useSelector(getCapabilites); + /* @conditional-compile-remove(together-mode) */ + const participantId = useSelector(getLocalUserId); + /* @conditional-compile-remove(together-mode) */ + const isTogetherModeActive = useSelector(getIsTogetherModeActive); const onSpeakerItemClick = useCallback( ( @@ -369,8 +377,29 @@ export const MoreDrawer = (props: MoreDrawerProps): JSX.Element => { secondaryIconProps: props.userSetGalleryLayout === 'default' ? { iconName: 'Accept' } : undefined }; + /* @conditional-compile-remove(together-mode) */ + const togetherModeOption = { + itemKey: 'togetherModeSelectionKey', + text: localeStrings.strings.call.moreButtonTogetherModeLayoutLabel, + onItemClick: () => { + props.onUserSetGalleryLayout && props.onUserSetGalleryLayout('togetherMode'); + onLightDismiss(); + }, + iconProps: { + iconName: 'TogetherModeLayout', + styles: { root: { lineHeight: 0 } } + }, + disabled: !( + (participantId?.kind === 'microsoftTeamsUser' && participantCapability?.startTogetherMode?.isPresent) || + isTogetherModeActive + ), + secondaryIconProps: props.userSetGalleryLayout === 'default' ? { iconName: 'Accept' } : undefined + }; + /* @conditional-compile-remove(gallery-layout-composite) */ galleryLayoutOptions.subMenuProps?.push(galleryOption); + /* @conditional-compile-remove(together-mode) */ + galleryLayoutOptions.subMenuProps?.push(togetherModeOption); if (drawerSelectionOptions !== false && isEnabled(drawerSelectionOptions?.galleryControlsButton)) { drawerMenuItems.push(galleryLayoutOptions); From 4423d4f9e00271b4bb41ddd3af06119067642ae7 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Mon, 30 Dec 2024 02:38:58 +0000 Subject: [PATCH 27/37] fix together mode stream swap --- .../src/handlers/createTeamsCallHandlers.ts | 6 +- .../src/utils/videoGalleryUtils.ts | 11 ++++ .../src/videoGallerySelector.ts | 14 +---- .../src/CallContext.ts | 2 +- .../src/TogetherModeSubscriber.ts | 58 ++++++++++--------- 5 files changed, 47 insertions(+), 44 deletions(-) diff --git a/packages/calling-component-bindings/src/handlers/createTeamsCallHandlers.ts b/packages/calling-component-bindings/src/handlers/createTeamsCallHandlers.ts index 7197392dd66..7727839b078 100644 --- a/packages/calling-component-bindings/src/handlers/createTeamsCallHandlers.ts +++ b/packages/calling-component-bindings/src/handlers/createTeamsCallHandlers.ts @@ -137,10 +137,8 @@ export const createDefaultTeamsCallingHandlers = memoizeOne( if (!callState) { return; } - if (!callState.togetherMode.isActive) { - const togetherModeFeature = call?.feature(Features.TogetherMode); - await togetherModeFeature?.start(); - } + const togetherModeFeature = call?.feature(Features.TogetherMode); + await togetherModeFeature?.start(); } }; } diff --git a/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts b/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts index b694420b586..3adeb5020ad 100644 --- a/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts +++ b/packages/calling-component-bindings/src/utils/videoGalleryUtils.ts @@ -265,3 +265,14 @@ export const memoizeLocalParticipant = memoizeOne( export const memoizeSpotlightedParticipantIds = memoizeOne((spotlightedParticipants) => spotlightedParticipants?.map((p: SpotlightedParticipant) => toFlatCommunicationIdentifier(p.identifier)) ); + +/** @private */ +export const memoizeTogetherModeStreams = memoizeOne((togetherModeStreams) => ({ + mainVideoStream: { + id: togetherModeStreams?.mainVideoStream?.id, + isReceiving: togetherModeStreams?.mainVideoStream?.isReceiving, + isAvailable: togetherModeStreams?.mainVideoStream?.isAvailable, + renderElement: togetherModeStreams?.mainVideoStream?.view?.target, + streamSize: togetherModeStreams?.mainVideoStream?.streamSize + } +})); diff --git a/packages/calling-component-bindings/src/videoGallerySelector.ts b/packages/calling-component-bindings/src/videoGallerySelector.ts index 1d0d2bedbe0..5e5cd00a91a 100644 --- a/packages/calling-component-bindings/src/videoGallerySelector.ts +++ b/packages/calling-component-bindings/src/videoGallerySelector.ts @@ -31,7 +31,8 @@ import { _videoGalleryRemoteParticipantsMemo, _dominantSpeakersWithFlatId, convertRemoteParticipantToVideoGalleryRemoteParticipant, - memoizeLocalParticipant + memoizeLocalParticipant, + memoizeTogetherModeStreams } from './utils/videoGalleryUtils'; import { memoizeSpotlightedParticipantIds } from './utils/videoGalleryUtils'; import { getLocalParticipantRaisedHand } from './baseSelectors'; @@ -121,15 +122,6 @@ export const videoGallerySelector: VideoGallerySelector = createSelector( const noRemoteParticipants: RemoteParticipantState[] = []; const localParticipantReactionState = memoizedConvertToVideoTileReaction(localParticipantReaction); const spotlightedParticipantIds = memoizeSpotlightedParticipantIds(spotlightCallFeature?.spotlightedParticipants); - /* @conditional-compile-remove(together-mode) */ - const togetherModeStreams = { - mainVideoStream: { - isReceiving: togetherModeCallFeature?.streams?.mainVideoStream?.isReceiving, - isAvailable: togetherModeCallFeature?.streams?.mainVideoStream?.isAvailable, - renderElement: togetherModeCallFeature?.streams?.mainVideoStream?.view?.target, - streamSize: togetherModeCallFeature?.streams?.mainVideoStream?.streamSize - } - }; return { screenShareParticipant: screenShareRemoteParticipant ? convertRemoteParticipantToVideoGalleryRemoteParticipant( @@ -174,7 +166,7 @@ export const videoGallerySelector: VideoGallerySelector = createSelector( spotlightedParticipants: spotlightedParticipantIds, maxParticipantsToSpotlight: spotlightCallFeature?.maxParticipantsToSpotlight, /* @conditional-compile-remove(together-mode) */ - togetherModeStreams: togetherModeStreams, + togetherModeStreams: memoizeTogetherModeStreams(togetherModeCallFeature?.streams), /* @conditional-compile-remove(together-mode) */ togetherModeSeatingCoordinates: togetherModeCallFeature?.seatingPositions, /* @conditional-compile-remove(together-mode) */ diff --git a/packages/calling-stateful-client/src/CallContext.ts b/packages/calling-stateful-client/src/CallContext.ts index c4abe43a11e..819291dc1bb 100644 --- a/packages/calling-stateful-client/src/CallContext.ts +++ b/packages/calling-stateful-client/src/CallContext.ts @@ -499,7 +499,7 @@ export class CallContext { if (call) { const stream = call.togetherMode.streams.mainVideoStream; if (stream && stream?.id === streamId) { - stream.isReceiving = isAvailable; + stream.isAvailable = isAvailable; } } }); diff --git a/packages/calling-stateful-client/src/TogetherModeSubscriber.ts b/packages/calling-stateful-client/src/TogetherModeSubscriber.ts index d9c0e08d9d0..8b5b3d66031 100644 --- a/packages/calling-stateful-client/src/TogetherModeSubscriber.ts +++ b/packages/calling-stateful-client/src/TogetherModeSubscriber.ts @@ -67,7 +67,9 @@ export class TogetherModeSubscriber { }; private addRemoteVideoStreamSubscriber = (togetherModeVideoStream: TogetherModeVideoStream): void => { - this._togetherModeVideoStreamSubscribers.get(togetherModeVideoStream.id)?.unsubscribe(); + if (this._togetherModeVideoStreamSubscribers.has(togetherModeVideoStream.id)) { + return; + } this._togetherModeVideoStreamSubscribers.set( togetherModeVideoStream.id, new TogetherModeVideoStreamSubscriber(this._callIdRef, togetherModeVideoStream, this._context) @@ -78,7 +80,7 @@ export class TogetherModeSubscriber { addedStreams: TogetherModeVideoStream[], removedStreams: TogetherModeVideoStream[] ): void => { - for (const stream of removedStreams) { + removedStreams.forEach((stream) => { this._togetherModeVideoStreamSubscribers.get(stream.id)?.unsubscribe(); this._togetherModeVideoStreamSubscribers.delete(stream.id); this._internalContext.deleteCallFeatureRenderInfo( @@ -86,39 +88,39 @@ export class TogetherModeSubscriber { this._featureName, stream.mediaStreamType ); - } + }); + + addedStreams + .filter((stream) => stream.isAvailable) + .forEach((stream) => { + this._internalContext.setCallFeatureRenderInfo( + this._callIdRef.callId, + this._featureName, + stream.mediaStreamType, + stream as RemoteVideoStream, + 'NotRendered', + undefined + ); + this.addRemoteVideoStreamSubscriber(stream); + }); - for (const stream of addedStreams) { - this._internalContext.setCallFeatureRenderInfo( - this._callIdRef.callId, - this._featureName, - stream.mediaStreamType, - stream as RemoteVideoStream, - 'NotRendered', - undefined - ); - this.addRemoteVideoStreamSubscriber(stream); - } this._context.setTogetherModeVideoStreams( this._callIdRef.callId, - addedStreams.map((stream) => - convertSdkCallFeatureStreamToDeclarativeCallFeatureStream(stream, this._featureName) - ), + addedStreams + .filter((stream) => stream.isAvailable) + .map((stream) => convertSdkCallFeatureStreamToDeclarativeCallFeatureStream(stream, this._featureName)), removedStreams.map((stream) => convertSdkCallFeatureStreamToDeclarativeCallFeatureStream(stream, this._featureName) ) ); - if (this._togetherMode.togetherModeStream.length) { - this._context.setLatestNotification(this._callIdRef.callId, { - target: 'togetherModeStarted', - timestamp: new Date(Date.now()) - }); - } else { - this._context.setLatestNotification(this._callIdRef.callId, { - target: 'togetherModeEnded', - timestamp: new Date(Date.now()) - }); - } + + const notificationTarget = this._togetherMode.togetherModeStream.length + ? 'togetherModeStarted' + : 'togetherModeEnded'; + this._context.setLatestNotification(this._callIdRef.callId, { + target: notificationTarget, + timestamp: new Date() + }); }; private onTogetherModeStreamUpdated = (args: { From bb11bc53638e82c16a11122d38966be111d244e8 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Tue, 31 Dec 2024 02:10:10 +0000 Subject: [PATCH 28/37] updated api --- .../communication-react/review/beta/communication-react.api.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index fca0c38142c..5acf0e334af 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -5558,13 +5558,10 @@ export interface VideoGalleryProps { // @deprecated (undocumented) onDisposeRemoteStreamView?: (userId: string) => Promise; onDisposeRemoteVideoStreamView?: (userId: string) => Promise; -<<<<<<< HEAD // (undocumented) onDisposeTogetherModeStreamView?: () => Promise; -======= onForbidAudio?: (userIds: string[]) => Promise; onForbidVideo?: (userIds: string[]) => Promise; ->>>>>>> 8eefe6caaa51dd5462d4371c1b0b5639c336ba92 onMuteParticipant?: (userId: string) => Promise; onPermitAudio?: (userIds: string[]) => Promise; onPermitVideo?: (userIds: string[]) => Promise; From 2aaa7e25646ec0d47bdd0aa0f4930a704f53fe48 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Sun, 5 Jan 2025 04:23:38 +0000 Subject: [PATCH 29/37] Together mode clean up --- .../src/components/TogetherModeOverlay.tsx | 278 ++++++++---------- .../VideoGallery/TogetherModeStream.tsx | 18 +- .../components/styles/TogetherMode.styles.ts | 37 ++- 3 files changed, 162 insertions(+), 171 deletions(-) diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index 21d0983872b..19ab35c9b47 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -20,7 +20,7 @@ import { // REACTION_START_DISPLAY_SIZE } from './VideoGallery/utils/reactionUtils'; /* @conditional-compile-remove(together-mode) */ -import { Icon, Text } from '@fluentui/react'; +import { Icon, mergeStyles, Stack, Text } from '@fluentui/react'; /* @conditional-compile-remove(together-mode) */ import { getEmojiResource } from './VideoGallery/utils/videoGalleryLayoutUtils'; /* @conditional-compile-remove(together-mode) */ @@ -86,53 +86,29 @@ export const TogetherModeOverlay = React.memo( }>({}); const [hoveredParticipantID, setHoveredParticipantID] = useState(''); - useMemo(() => { - const removedVisibleParticipants = Object.keys(togetherModeParticipantStatus).filter( - (participantId) => !togetherModeSeatPositions[participantId] - ); - // Update visible signals state instead of directly mutating it - setTogetherModeParticipantStatus((prevSignals) => { - const newSignals = { ...prevSignals }; - removedVisibleParticipants.forEach((participantId) => { - delete newSignals[participantId]; - }); - - // Trigger a re-render only if changes occurred - const hasChanges = Object.keys(newSignals).length !== Object.keys(prevSignals).length; - if (hasChanges) { - return newSignals; - } - return prevSignals; - }); - }, [togetherModeParticipantStatus, togetherModeSeatPositions]); - useMemo(() => { const allParticipants = [...remoteParticipants, localParticipant]; - const participantsWithVideoAvailable = allParticipants.filter( - (p) => p.videoStream?.isAvailable && togetherModeSeatPositions[p.userId] - ); - const updatedSignals = participantsWithVideoAvailable.reduce( - (acc: { [key: string]: TogetherModeParticipantStatus }, p: VideoGalleryLocalParticipant) => { - const { userId, reaction, raisedHand, spotlight, isMuted, displayName } = p; - const seatingPosition = togetherModeSeatPositions[userId]; - if (seatingPosition) { - acc[userId] = { - id: userId, - reaction: reactionResources && reaction, - isHandRaised: !!raisedHand, - isSpotlighted: !!spotlight, - isMuted, - displayName: displayName || locale.strings.videoGallery.displayNamePlaceholder, - showDisplayName: !!(spotlight || raisedHand || hoveredParticipantID === userId), - scaledSize: calculateScaledSize(seatingPosition.width, seatingPosition.height), - seatPositionStyle: setTogetherModeSeatPositionStyle(seatingPosition) - }; - } - return acc; - }, - {} - ); + const participantsWithVideoAvailable = allParticipants.filter((p) => togetherModeSeatPositions[p.userId]); + + const updatedSignals: { [key: string]: TogetherModeParticipantStatus } = {}; + for (const p of participantsWithVideoAvailable) { + const { userId, reaction, raisedHand, spotlight, isMuted, displayName } = p; + const seatingPosition = togetherModeSeatPositions[userId]; + if (seatingPosition) { + updatedSignals[userId] = { + id: userId, + reaction: reactionResources && reaction, + isHandRaised: !!raisedHand, + isSpotlighted: !!spotlight, + isMuted, + displayName: displayName || locale.strings.videoGallery.displayNamePlaceholder, + showDisplayName: !!(spotlight || raisedHand || hoveredParticipantID === userId), + scaledSize: calculateScaledSize(seatingPosition.width, seatingPosition.height), + seatPositionStyle: setTogetherModeSeatPositionStyle(seatingPosition) + }; + } + } const participantsNotInTogetherModeStream = Object.keys(togetherModeParticipantStatus).filter( (id) => !updatedSignals[id] @@ -141,6 +117,7 @@ export const TogetherModeOverlay = React.memo( setTogetherModeParticipantStatus((prevSignals) => { const newSignals = { ...prevSignals, ...updatedSignals }; const newSignalsLength = Object.keys(newSignals).length; + participantsNotInTogetherModeStream.forEach((id) => { delete newSignals[id]; }); @@ -163,123 +140,124 @@ export const TogetherModeOverlay = React.memo( hoveredParticipantID ]); + // When a 50-participant scene switches to a smaller group in Together Mode, signals for those no longer in the stream are removed + useMemo(() => { + const removedVisibleParticipants = Object.keys(togetherModeParticipantStatus).filter( + (participantId) => !togetherModeSeatPositions[participantId] + ); + + setTogetherModeParticipantStatus((prevSignals) => { + const newSignals = { ...prevSignals }; + removedVisibleParticipants.forEach((participantId) => { + delete newSignals[participantId]; + }); + + // Trigger a re-render only if changes occurred + const hasChanges = Object.keys(newSignals).length !== Object.keys(prevSignals).length; + return hasChanges ? newSignals : prevSignals; + }); + }, [togetherModeParticipantStatus, togetherModeSeatPositions]); + return (
- {Object.values(togetherModeParticipantStatus).map((participantStatus) => ( -
setHoveredParticipantID(participantStatus.id)} - onMouseLeave={() => setHoveredParticipantID('')} - > -
- {participantStatus.reaction?.reactionType && ( -
-
+ {Object.values(togetherModeParticipantStatus).map( + (participantStatus) => + participantStatus.id && ( +
setHoveredParticipantID(participantStatus.id)} + onMouseLeave={() => setHoveredParticipantID('')} + > +
+ {participantStatus.reaction?.reactionType && (
-
-
- )} - - {participantStatus.showDisplayName && ( -
-
- {participantStatus.isHandRaised && ( - +
- - - )} - {participantStatus.showDisplayName && ( - +
+
+ )} + + {participantStatus.showDisplayName && ( +
+
- {participantStatus.displayName} - - )} - {participantStatus.isMuted && ( - - - - )} - {participantStatus.isSpotlighted && ( - - - - )} -
+ {participantStatus.isHandRaised && } + {participantStatus.showDisplayName && ( + + {participantStatus.displayName} + + )} + {participantStatus.isMuted && ( + + + + )} + {participantStatus.isSpotlighted && ( + + + + )} +
+
+ )}
- )} -
-
- ))} +
+ ) + )}
); } diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx index b828c0d2619..ed3f215a303 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx @@ -54,6 +54,10 @@ export const TogetherModeStream = React.memo( onSetTogetherModeSceneSize, onDisposeTogetherModeStreamView, togetherModeStreams, + seatingCoordinates, + localParticipant, + remoteParticipants, + reactionResources, containerWidth, containerHeight } = props; @@ -86,6 +90,7 @@ export const TogetherModeStream = React.memo( } }, [onSetTogetherModeSceneSize, containerWidth, containerHeight]); + console.log(`CHUK seatingCoordinates: ${JSON.stringify(seatingCoordinates)}`); const stream = props.togetherModeStreams?.mainVideoStream; const showLoadingIndicator = !(stream && stream.isAvailable && stream.isReceiving); return containerWidth && containerHeight ? ( @@ -94,17 +99,12 @@ export const TogetherModeStream = React.memo( videoStreamElement={stream?.renderElement || null} isMirrored={true} loadingState={showLoadingIndicator ? 'loading' : 'none'} - styles={{ - root: { - border: '2px solid yellow' - } - }} /> diff --git a/packages/react-components/src/components/styles/TogetherMode.styles.ts b/packages/react-components/src/components/styles/TogetherMode.styles.ts index cda9f65f4ea..9a9b538cf66 100644 --- a/packages/react-components/src/components/styles/TogetherMode.styles.ts +++ b/packages/react-components/src/components/styles/TogetherMode.styles.ts @@ -5,7 +5,7 @@ import { _pxToRem } from '@internal/acs-ui-common'; /* @conditional-compile-remove(together-mode) */ import { VideoGalleryTogetherModeSeatingInfo } from '../../types/TogetherModeTypes'; /* @conditional-compile-remove(together-mode) */ -import { IStackStyles } from '@fluentui/react'; +import { IStackStyles, IStyle } from '@fluentui/react'; import React from 'react'; /* @conditional-compile-remove(together-mode) */ @@ -163,18 +163,24 @@ export const togetherModeParticipantDisplayName = ( participantSeatingWidth: number, color: string ): React.CSSProperties => { - console.log(`CHUK ---Paarticipant width == ${participantSeatingWidth}`); + const width = + isParticipantHovered || participantSeatingWidth * 16 > 100 + ? 'fit-content' + : _pxToRem(0.7 * participantSeatingWidth * 16); + + const display = isParticipantHovered || participantSeatingWidth * 16 > 150 ? 'inline-block' : 'none'; + return { textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center', color, overflow: isParticipantHovered ? 'visible' : 'hidden', - width: isParticipantHovered ? `100%` : _pxToRem(0.7 * participantSeatingWidth * 16), - // border: '1px solid red', - display: isParticipantHovered || participantSeatingWidth * 16 > 150 ? 'inline-block' : 'none', // Completely remove the element when hidden + width, + display, fontSize: `${_pxToRem(13)}`, - lineHeight: `${_pxToRem(20)}` + lineHeight: `${_pxToRem(20)}`, + maxWidth: isParticipantHovered ? 'fit-content' : _pxToRem(0.7 * participantSeatingWidth * 16) }; }; @@ -182,10 +188,17 @@ export const togetherModeParticipantDisplayName = ( /** * @private */ -export const togetherModeIconStyle = (): React.CSSProperties => { - return { - width: 'fit-content', - display: 'flex', - alignItems: 'center' - }; +export const togetherModeIconStyle: IStyle = { + margin: 'auto', + alignItems: 'center', + '& svg': { + display: 'block', + // Similar to text color, icon color will be inherited from parent container + color: 'inherit' + }, + + width: 'fit-content', + // display: 'flex', + size: `${_pxToRem(100)}`, + fontSize: `${_pxToRem(100)}` }; From cd2d6fb7a4d235ec72747cb0d72c1063ef5b39d6 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Thu, 9 Jan 2025 02:24:58 +0000 Subject: [PATCH 30/37] Updated notification strings --- .../review/beta/communication-react.api.md | 2 -- .../src/composites/CallComposite/Strings.tsx | 12 ------------ 2 files changed, 14 deletions(-) diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index 84d4fdaa25e..6a667bb2a51 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -1036,8 +1036,6 @@ export interface CallCompositeStrings { tagsSurveyTextFieldDefaultText: string; threeParticipantJoinedNoticeString: string; threeParticipantLeftNoticeString: string; - togetherModeEnded?: string; - togetherModeStarted?: string; transferPageNoticeString: string; transferPageTransferorText: string; transferPageTransferTargetText: string; diff --git a/packages/react-composites/src/composites/CallComposite/Strings.tsx b/packages/react-composites/src/composites/CallComposite/Strings.tsx index 80704c2af90..1152b8dd2cf 100644 --- a/packages/react-composites/src/composites/CallComposite/Strings.tsx +++ b/packages/react-composites/src/composites/CallComposite/Strings.tsx @@ -922,18 +922,6 @@ export interface CallCompositeStrings { * notification. */ returnFromBreakoutRoomBannerButtonLabel: string; - /* @conditional-compile-remove(together-mode) */ - /** - * Label for button in banner to return from breakout room. The banner is shown in mobile view instead of the - * notification. - */ - togetherModeStarted?: string; - /* @conditional-compile-remove(together-mode) */ - /** - * Label for button in banner to return from breakout room. The banner is shown in mobile view instead of the - * notification. - */ - togetherModeEnded?: string; /* @conditional-compile-remove(media-access) */ /** * Label for menu item to forbid audio media access From 7cf834ee2265650407ba446128ebc6255b764a11 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Thu, 9 Jan 2025 12:18:19 +0000 Subject: [PATCH 31/37] Included conditioal compile statements --- .../src/videoGallerySelector.ts | 2 +- .../src/components/MeetingReactionOverlay.tsx | 10 ++++++++-- .../src/components/TogetherModeOverlay.tsx | 2 ++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/calling-component-bindings/src/videoGallerySelector.ts b/packages/calling-component-bindings/src/videoGallerySelector.ts index 5e5cd00a91a..39357cd424d 100644 --- a/packages/calling-component-bindings/src/videoGallerySelector.ts +++ b/packages/calling-component-bindings/src/videoGallerySelector.ts @@ -32,7 +32,7 @@ import { _dominantSpeakersWithFlatId, convertRemoteParticipantToVideoGalleryRemoteParticipant, memoizeLocalParticipant, - memoizeTogetherModeStreams + /* @conditional-compile-remove(together-mode) */ memoizeTogetherModeStreams } from './utils/videoGalleryUtils'; import { memoizeSpotlightedParticipantIds } from './utils/videoGalleryUtils'; import { getLocalParticipantRaisedHand } from './baseSelectors'; diff --git a/packages/react-components/src/components/MeetingReactionOverlay.tsx b/packages/react-components/src/components/MeetingReactionOverlay.tsx index 73af85e785a..b7e887a0767 100644 --- a/packages/react-components/src/components/MeetingReactionOverlay.tsx +++ b/packages/react-components/src/components/MeetingReactionOverlay.tsx @@ -75,8 +75,14 @@ const REACTION_EMOJI_RESIZE_SCALE_CONSTANT = 3; * @internal */ export const MeetingReactionOverlay = (props: MeetingReactionOverlayProps): JSX.Element => { - const { overlayMode, reaction, reactionResources, localParticipant, remoteParticipants, togetherModeSeatPositions } = - props; + const { + overlayMode, + reaction, + reactionResources, + localParticipant, + remoteParticipants, + /* @conditional-compile-remove(together-mode) */ togetherModeSeatPositions + } = props; const [emojiSizePx, setEmojiSizePx] = useState(0); const [divHeight, setDivHeight] = useState(0); const [divWidth, setDivWidth] = useState(0); diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index 19ab35c9b47..c3b2cfed043 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -37,8 +37,10 @@ import { togetherModeParticipantStatusContainer, TogetherModeSeatStyle } from './styles/TogetherMode.styles'; +/* @conditional-compile-remove(together-mode) */ import { CallingTheme, useTheme } from '../theming'; // import { iconContainerStyle, raiseHandContainerStyles } from './styles/VideoTile.styles'; +/* @conditional-compile-remove(together-mode) */ import { RaisedHandIcon } from './assets/RaisedHandIcon'; /* @conditional-compile-remove(together-mode) */ import { _pxToRem } from '@internal/acs-ui-common'; From 701be984380ab4402b3411e1e70069509ced15b9 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Thu, 9 Jan 2025 21:43:38 +0000 Subject: [PATCH 32/37] Fix signaling status --- .../review/beta/communication-react.api.md | 2 +- .../src/components/TogetherModeOverlay.tsx | 23 ++++++++++--------- .../VideoGallery/TogetherModeLayout.tsx | 2 +- .../CallComposite/components/Prompt.tsx | 2 +- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index 6a667bb2a51..722f6ddb21f 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -5518,6 +5518,7 @@ export type VideoGalleryParticipant = { mediaAccess?: MediaAccess; canAudioBeForbidden?: boolean; canVideoBeForbidden?: boolean; + isSpeaking?: boolean; }; // @public @@ -5583,7 +5584,6 @@ export interface VideoGalleryProps { // @public export interface VideoGalleryRemoteParticipant extends VideoGalleryParticipant { - isSpeaking?: boolean; mediaAccess?: MediaAccess; raisedHand?: RaisedHand; reaction?: Reaction; diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index c3b2cfed043..d1f3523f4a6 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -90,8 +90,9 @@ export const TogetherModeOverlay = React.memo( useMemo(() => { const allParticipants = [...remoteParticipants, localParticipant]; - - const participantsWithVideoAvailable = allParticipants.filter((p) => togetherModeSeatPositions[p.userId]); + const participantsWithVideoAvailable = allParticipants.filter( + (p) => p.videoStream?.isAvailable && togetherModeSeatPositions[p.userId] + ); const updatedSignals: { [key: string]: TogetherModeParticipantStatus } = {}; for (const p of participantsWithVideoAvailable) { @@ -118,19 +119,18 @@ export const TogetherModeOverlay = React.memo( setTogetherModeParticipantStatus((prevSignals) => { const newSignals = { ...prevSignals, ...updatedSignals }; - const newSignalsLength = Object.keys(newSignals).length; participantsNotInTogetherModeStream.forEach((id) => { delete newSignals[id]; }); - const hasChanges = Object.keys(newSignals).some( - (key) => - JSON.stringify(newSignals[key]) !== JSON.stringify(prevSignals[key]) || - newSignalsLength !== Object.keys(prevSignals).length + const hasSignalingChange = Object.keys(newSignals).some( + (key) => JSON.stringify(newSignals[key]) !== JSON.stringify(prevSignals[key]) ); - return hasChanges ? newSignals : prevSignals; + const updateTogetherModeParticipantStatusState = + hasSignalingChange || Object.keys(newSignals).length !== Object.keys(prevSignals).length; + return updateTogetherModeParticipantStatusState ? newSignals : prevSignals; }); }, [ remoteParticipants, @@ -155,8 +155,9 @@ export const TogetherModeOverlay = React.memo( }); // Trigger a re-render only if changes occurred - const hasChanges = Object.keys(newSignals).length !== Object.keys(prevSignals).length; - return hasChanges ? newSignals : prevSignals; + const updateTogetherModeParticipantStatusState = + Object.keys(newSignals).length !== Object.keys(prevSignals).length; + return updateTogetherModeParticipantStatusState ? newSignals : prevSignals; }); }, [togetherModeParticipantStatus, togetherModeSeatPositions]); @@ -169,7 +170,7 @@ export const TogetherModeOverlay = React.memo( key={participantStatus.id} style={{ ...getTogetherModeParticipantOverlayStyle(participantStatus.seatPositionStyle), - border: '1px solid red' + border: '1px solid blue' }} onMouseEnter={() => setHoveredParticipantID(participantStatus.id)} onMouseLeave={() => setHoveredParticipantID('')} diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx index 7a2be79e46d..64d72434518 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx @@ -114,7 +114,7 @@ export const TogetherModeLayout = (props: LayoutProps): JSX.Element => { parentWidth ]); - console.log(`CHUK == version === mobile fix`); + console.log(`CHUK == version === BUG-BASH-1.1`); return screenShareComponent ? ( diff --git a/packages/react-composites/src/composites/CallComposite/components/Prompt.tsx b/packages/react-composites/src/composites/CallComposite/components/Prompt.tsx index 8fd0d975974..2440a24b0a8 100644 --- a/packages/react-composites/src/composites/CallComposite/components/Prompt.tsx +++ b/packages/react-composites/src/composites/CallComposite/components/Prompt.tsx @@ -142,5 +142,5 @@ export interface SpotlightPromptStrings { /** * Label for button to close prompt */ - closeSpotlightPromptButtonLabel?: string; + closeSpotlightPromptButtonLabel: string; } From e6af06ba95659822d9a2bbd66e0effc0b6797b58 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Thu, 9 Jan 2025 22:08:59 +0000 Subject: [PATCH 33/37] Reverted change --- .../src/composites/localization/locales/en-US/strings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-composites/src/composites/localization/locales/en-US/strings.json b/packages/react-composites/src/composites/localization/locales/en-US/strings.json index e06ac880a65..099720ef486 100644 --- a/packages/react-composites/src/composites/localization/locales/en-US/strings.json +++ b/packages/react-composites/src/composites/localization/locales/en-US/strings.json @@ -335,7 +335,8 @@ "stopAllSpotlightText": "The videos will no longer be highlighted for everyone in the meeting.", "stopSpotlightConfirmButtonLabel": "Stop spotlighting", "stopSpotlightOnSelfConfirmButtonLabel": "Exit spotlight", - "stopSpotlightCancelButtonLabel": "Cancel" + "stopSpotlightCancelButtonLabel": "Cancel", + "closeSpotlightPromptButtonLabel": "Close" }, "exitSpotlightButtonLabel": "Exit spotlight", "exitSpotlightButtonTooltip": "Exit spotlight", From a9bca930f03263e64c6e0b002b0b5fc4df74d9bc Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Fri, 10 Jan 2025 16:49:25 +0000 Subject: [PATCH 34/37] Updated api file --- .../review/beta/communication-react.api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index 4c5b950a2c3..127762e409a 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -4967,7 +4967,7 @@ export type SpotlightChangedListener = (args: { // @public export interface SpotlightPromptStrings { - closeSpotlightPromptButtonLabel?: string; + closeSpotlightPromptButtonLabel: string; startSpotlightCancelButtonLabel: string; startSpotlightConfirmButtonLabel: string; startSpotlightHeading: string; @@ -5520,7 +5520,6 @@ export type VideoGalleryParticipant = { mediaAccess?: MediaAccess; canAudioBeForbidden?: boolean; canVideoBeForbidden?: boolean; - isSpeaking?: boolean; }; // @public @@ -5586,6 +5585,7 @@ export interface VideoGalleryProps { // @public export interface VideoGalleryRemoteParticipant extends VideoGalleryParticipant { + isSpeaking?: boolean; mediaAccess?: MediaAccess; raisedHand?: RaisedHand; reaction?: Reaction; From 478c8ae273a63f11be32709bcedde929f4e3bcea Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Tue, 14 Jan 2025 06:04:50 +0000 Subject: [PATCH 35/37] Cleanup --- .../src/components/TogetherModeOverlay.tsx | 3 +- .../components/styles/TogetherMode.styles.ts | 40 +++++++++++++++---- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index 3b99233462f..fc30c28cb61 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -27,6 +27,7 @@ import { _HighContrastAwareIcon } from './HighContrastAwareIcon'; import { calculateScaledSize, getTogetherModeParticipantOverlayStyle, + participantStatusTransitionStyle, REACTION_MAX_TRAVEL_HEIGHT, REACTION_TRAVEL_HEIGHT, setTogetherModeSeatPositionStyle, @@ -218,7 +219,7 @@ export const TogetherModeOverlay = memo( )} {participantStatus.showDisplayName && ( -
+
{ - const MIN_DISPLAY_NAME_WIDTH = 100; +): React.CSSProperties => { + const width = + isParticipantHovered || participantSeatingWidth * 16 > 100 + ? 'fit-content' + : _pxToRem(0.7 * participantSeatingWidth * 16); + + const display = isParticipantHovered || participantSeatingWidth * 16 > 150 ? 'inline-block' : 'none'; + return { textOverflow: 'ellipsis', - flexGrow: 1, // Allow text to grow within available space - overflow: isParticipantHovered ? 'visible' : 'hidden', whiteSpace: 'nowrap', textAlign: 'center', color, - display: isParticipantHovered || participantSeatingWidth > MIN_DISPLAY_NAME_WIDTH ? 'inline-block' : 'none' // Completely remove the element when hidden + overflow: isParticipantHovered ? 'visible' : 'hidden', + width, + display, + fontSize: `${_pxToRem(13)}`, + lineHeight: `${_pxToRem(20)}`, + maxWidth: isParticipantHovered ? 'fit-content' : _pxToRem(0.7 * participantSeatingWidth * 16) }; }; @@ -221,11 +230,28 @@ export const togetherModeParticipantEmojiSpriteStyle = ( participantSeatWidth: string ): CSSProperties => { const participantSeatWidthInPixel = parseFloat(participantSeatWidth) * REM_TO_PX_MULTIPLIER; - const emojiScaledSizeInPercent = (emojiScaledSize / participantSeatWidthInPixel) * 100; + const emojiScaledSizeInPercent = 100 - (emojiScaledSize / participantSeatWidthInPixel) * 100; return { width: `${emojiSize}`, position: 'absolute', // Center the emoji sprite within the participant seat - left: `${emojiScaledSizeInPercent / 2}%` + left: `${emojiScaledSizeInPercent / 2}%`, + zIndex: 3 }; }; + +/** + * The style for the transition of the participant status container in Together Mode. + * @private + */ +export const participantStatusTransitionStyle: CSSProperties = { + position: 'absolute', + bottom: `${_pxToRem(2)}`, + width: 'fit-content', + textAlign: 'center', + border: '1px solid white', + transform: 'translate(-50%)', + transition: 'width 0.3s ease, transform 0.3s ease', + left: '50%', + zIndex: 0 +}; From 2db6ad6674a53eb011394b31a113146121b00dc9 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Tue, 14 Jan 2025 18:27:00 +0000 Subject: [PATCH 36/37] accessibility fix --- .../src/components/TogetherModeOverlay.tsx | 84 ++++++++++--------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index fc30c28cb61..6b923a94459 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -2,7 +2,7 @@ // Licensed under the MIT License. /* @conditional-compile-remove(together-mode) */ -import React, { useMemo, useState, memo } from 'react'; +import React, { useMemo, useState, memo, useEffect } from 'react'; /* @conditional-compile-remove(together-mode) */ import { Reaction, @@ -84,13 +84,22 @@ export const TogetherModeOverlay = memo( [key: string]: TogetherModeParticipantStatus; }>({}); const [hoveredParticipantID, setHoveredParticipantID] = useState(''); + const [tabbedParticipantID, setTabbedParticipantID] = useState(''); + // const [tabFocused, setTabFocused] = useState(false); + + // Reset the Tab key tracking on any other key press + const handleKeyUp = (e, participantId: string) => { + if (e.key === 'Tab') { + setTabbedParticipantID(participantId); + } + }; /* * The useMemo hook is used to calculate the participant status for the Together Mode overlay. * It updates the togetherModeParticipantStatus state when there's a change in the remoteParticipants, localParticipant, * raisedHand, spotlight, isMuted, displayName, or hoveredParticipantID. */ - useMemo(() => { + const updatedParticipantStatus = useMemo(() => { const allParticipants = [...remoteParticipants, localParticipant]; const participantsWithVideoAvailable = allParticipants.filter( @@ -109,7 +118,12 @@ export const TogetherModeOverlay = memo( isSpotlighted: !!spotlight, isMuted, displayName: displayName || locale.strings.videoGallery.displayNamePlaceholder, - showDisplayName: !!(spotlight || raisedHand || hoveredParticipantID === userId), + showDisplayName: !!( + spotlight || + raisedHand || + hoveredParticipantID === userId || + tabbedParticipantID === userId + ), scaledSize: calculateScaledSize(seatingPosition.width, seatingPosition.height), seatPositionStyle: setTogetherModeSeatPositionStyle(seatingPosition) }; @@ -121,21 +135,19 @@ export const TogetherModeOverlay = memo( (id) => !updatedSignals[id] ); - setTogetherModeParticipantStatus((prevSignals) => { - const newSignals = { ...prevSignals, ...updatedSignals }; + const newSignals = { ...togetherModeParticipantStatus, ...updatedSignals }; - participantsNotInTogetherModeStream.forEach((id) => { - delete newSignals[id]; - }); + participantsNotInTogetherModeStream.forEach((id) => { + delete newSignals[id]; + }); - const hasSignalingChange = Object.keys(newSignals).some( - (key) => JSON.stringify(newSignals[key]) !== JSON.stringify(prevSignals[key]) - ); + const hasSignalingChange = Object.keys(newSignals).some( + (key) => JSON.stringify(newSignals[key]) !== JSON.stringify(togetherModeParticipantStatus[key]) + ); - const updateTogetherModeParticipantStatusState = - hasSignalingChange || Object.keys(newSignals).length !== Object.keys(prevSignals).length; - return updateTogetherModeParticipantStatusState ? newSignals : prevSignals; - }); + const updateTogetherModeParticipantStatusState = + hasSignalingChange || Object.keys(newSignals).length !== Object.keys(togetherModeParticipantStatus).length; + return updateTogetherModeParticipantStatusState ? newSignals : togetherModeParticipantStatus; }, [ remoteParticipants, localParticipant, @@ -143,46 +155,38 @@ export const TogetherModeOverlay = memo( togetherModeSeatPositions, reactionResources, locale.strings.videoGallery.displayNamePlaceholder, - hoveredParticipantID + hoveredParticipantID, + tabbedParticipantID ]); - /* - * When a larger participant scene switches to a smaller group in Together Mode, - * participant video streams remain available because their video is still active, - * even though they are not visible in the Together Mode stream. - * Therefore, we rely on the updated seating position values to identify who is included in the Together Mode stream. - * The Together mode seat position will only contain seat coordinates of participants who are visible in the Together Mode stream. - */ - useMemo(() => { - const removedVisibleParticipants = Object.keys(togetherModeParticipantStatus).filter( - (participantId) => !togetherModeSeatPositions[participantId] - ); + useEffect(() => { + if (hoveredParticipantID && !updatedParticipantStatus[hoveredParticipantID]) { + setHoveredParticipantID(''); + } - setTogetherModeParticipantStatus((prevSignals) => { - const newSignals = { ...prevSignals }; - removedVisibleParticipants.forEach((participantId) => { - delete newSignals[participantId]; - }); + if (tabbedParticipantID && !updatedParticipantStatus[tabbedParticipantID]) { + setTabbedParticipantID(''); + } - // Trigger a re-render only if changes occurred - const updateTogetherModeParticipantStatusState = - Object.keys(newSignals).length !== Object.keys(prevSignals).length; - return updateTogetherModeParticipantStatusState ? newSignals : prevSignals; - }); - }, [togetherModeParticipantStatus, togetherModeSeatPositions]); + setTogetherModeParticipantStatus(updatedParticipantStatus); + }, [hoveredParticipantID, tabbedParticipantID, updatedParticipantStatus]); return (
{Object.values(togetherModeParticipantStatus).map( - (participantStatus) => + (participantStatus, index) => participantStatus.id && (
setHoveredParticipantID(participantStatus.id)} onMouseLeave={() => setHoveredParticipantID('')} + onKeyUp={(e) => handleKeyUp(e, participantStatus.id)} + onBlur={() => setTabbedParticipantID('')} + tabIndex={index} >
{participantStatus.reaction?.reactionType && ( @@ -219,7 +223,7 @@ export const TogetherModeOverlay = memo( )} {participantStatus.showDisplayName && ( -
+
Date: Fri, 17 Jan 2025 22:14:09 +0000 Subject: [PATCH 37/37] Addressed comments --- .../src/components/TogetherModeOverlay.tsx | 3 +- .../src/components/VideoGallery.tsx | 4 +- .../VideoGallery/TogetherModeLayout.tsx | 130 +----------------- .../VideoGallery/TogetherModeStream.tsx | 1 - 4 files changed, 7 insertions(+), 131 deletions(-) diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index 6b923a94459..ece9392cec2 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -85,10 +85,9 @@ export const TogetherModeOverlay = memo( }>({}); const [hoveredParticipantID, setHoveredParticipantID] = useState(''); const [tabbedParticipantID, setTabbedParticipantID] = useState(''); - // const [tabFocused, setTabFocused] = useState(false); // Reset the Tab key tracking on any other key press - const handleKeyUp = (e, participantId: string) => { + const handleKeyUp = (e: React.KeyboardEvent, participantId: string) => { if (e.key === 'Tab') { setTabbedParticipantID(participantId); } diff --git a/packages/react-components/src/components/VideoGallery.tsx b/packages/react-components/src/components/VideoGallery.tsx index 82fef7a1f90..dbd969ab695 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -815,7 +815,6 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { localParticipant={localParticipant} remoteParticipants={remoteParticipants} reactionResources={reactionResources} - screenShareComponent={screenShareComponent} containerWidth={containerWidth} containerHeight={containerHeight} /> @@ -832,7 +831,6 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { localParticipant, remoteParticipants, reactionResources, - screenShareComponent, containerWidth, containerHeight ] @@ -903,7 +901,7 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { /* @conditional-compile-remove(together-mode) */ // Teams users can switch to Together mode layout only if they have the capability, // while ACS users can do so only if Together mode is enabled. - if (layout === 'togetherMode' && canSwitchToTogetherModeLayout) { + if (!screenShareComponent && layout === 'togetherMode' && canSwitchToTogetherModeLayout) { return ; } return ; diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx index 64d72434518..aae61d8ee3d 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx @@ -2,27 +2,13 @@ // Licensed under the MIT License. /* @conditional-compile-remove(together-mode) */ -import React, { useMemo, useRef, useState } from 'react'; -/* @conditional-compile-remove(together-mode) */ -import { useId } from '@fluentui/react-hooks'; +import React from 'react'; /* @conditional-compile-remove(together-mode) */ import { _formatString } from '@internal/acs-ui-common'; /* @conditional-compile-remove(together-mode) */ import { LayoutProps } from './Layout'; /* @conditional-compile-remove(together-mode) */ -import { LayerHost, mergeStyles, Stack } from '@fluentui/react'; -/* @conditional-compile-remove(together-mode) */ -import { renderTiles, useOrganizedParticipants } from './utils/videoGalleryLayoutUtils'; -/* @conditional-compile-remove(together-mode) */ -import { OverflowGallery } from './OverflowGallery'; -/* @conditional-compile-remove(together-mode) */ -import { rootLayoutStyle } from './styles/DefaultLayout.styles'; -/* @conditional-compile-remove(together-mode) */ -import { isNarrowWidth, isShortHeight } from '../utils/responsive'; -/* @conditional-compile-remove(together-mode) */ -import { innerLayoutStyle, layerHostStyle } from './styles/FloatingLocalVideoLayout.styles'; -/* @conditional-compile-remove(together-mode) */ -import { videoGalleryLayoutGap } from './styles/Layout.styles'; +import { Stack } from '@fluentui/react'; /* @conditional-compile-remove(together-mode) */ /** @@ -31,113 +17,7 @@ import { videoGalleryLayoutGap } from './styles/Layout.styles'; * https://reactjs.org/docs/react-api.html#reactmemo */ export const TogetherModeLayout = (props: LayoutProps): JSX.Element => { - const { - remoteParticipants = [], - dominantSpeakers, - screenShareComponent, - onRenderRemoteParticipant, - styles, - maxRemoteVideoStreams, - parentWidth, - parentHeight, - overflowGalleryPosition = 'horizontalBottom', - pinnedParticipantUserIds = [], - togetherModeStreamComponent - } = props; - const isNarrow = parentWidth ? isNarrowWidth(parentWidth) : false; - - const isShort = parentHeight ? isShortHeight(parentHeight) : false; - - const [indexesToRender, setIndexesToRender] = useState([]); - const childrenPerPage = useRef(4); - - const { gridParticipants, overflowGalleryParticipants } = useOrganizedParticipants({ - remoteParticipants, - dominantSpeakers, - maxGridParticipants: maxRemoteVideoStreams, - isScreenShareActive: !!screenShareComponent, - maxOverflowGalleryDominantSpeakers: screenShareComponent - ? childrenPerPage.current - (pinnedParticipantUserIds.length % childrenPerPage.current) - : childrenPerPage.current, - pinnedParticipantUserIds, - layout: 'floatingLocalVideo' - }); - const { gridTiles, overflowGalleryTiles } = renderTiles( - gridParticipants, - onRenderRemoteParticipant, - maxRemoteVideoStreams, - indexesToRender, - overflowGalleryParticipants, - dominantSpeakers - ); - - const layerHostId = useId('layerhost'); - const togetherModeOverFlowGalleryTiles = useMemo(() => { - let newTiles = overflowGalleryTiles; - if (togetherModeStreamComponent) { - if (screenShareComponent) { - newTiles = gridTiles.concat(overflowGalleryTiles); - } - } - return newTiles; - }, [gridTiles, overflowGalleryTiles, screenShareComponent, togetherModeStreamComponent]); - - const overflowGallery = useMemo(() => { - if (overflowGalleryTiles.length === 0 && !props.screenShareComponent) { - return null; - } - return ( - { - childrenPerPage.current = n; - }} - parentWidth={parentWidth} - /> - ); - }, [ - overflowGalleryTiles.length, - props.screenShareComponent, - isShort, - isNarrow, - togetherModeOverFlowGalleryTiles, - styles?.horizontalGallery, - styles?.verticalGallery, - overflowGalleryPosition, - parentWidth - ]); - - console.log(`CHUK == version === BUG-BASH-1.1`); - return screenShareComponent ? ( - - - - {props.overflowGalleryPosition === 'horizontalTop' ? overflowGallery : <>} - {screenShareComponent} - {overflowGalleryTrampoline(overflowGallery, props.overflowGalleryPosition)} - - - ) : ( - {props.togetherModeStreamComponent} - ); -}; - -/* @conditional-compile-remove(together-mode) */ -const overflowGalleryTrampoline = ( - gallery: JSX.Element | null, - galleryPosition?: 'horizontalBottom' | 'verticalRight' | 'horizontalTop' -): JSX.Element | null => { - return galleryPosition !== 'horizontalTop' ? gallery : <>; - return gallery; + const { togetherModeStreamComponent } = props; + console.log(`TogetherModeLayout: CHUK-1`); + return {togetherModeStreamComponent}; }; diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx index 521a58aa2dc..d81639b1112 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx @@ -42,7 +42,6 @@ export const TogetherModeStream = memo( reactionResources?: ReactionResources; localParticipant?: VideoGalleryLocalParticipant; remoteParticipants?: VideoGalleryRemoteParticipant[]; - screenShareComponent?: JSX.Element; containerWidth?: number; containerHeight?: number; }): JSX.Element => {