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/handlers/createCommonHandlers.ts b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts index dd56579db8e..8dff755dc63 100644 --- a/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts +++ b/packages/calling-component-bindings/src/handlers/createCommonHandlers.ts @@ -821,6 +821,7 @@ export const createDefaultCommonCallingHandlers = memoizeOne( } const togetherModeStreams = callState.togetherMode.streams; + if (!togetherModeStreams.mainVideoStream) { return; } diff --git a/packages/calling-stateful-client/src/CallContext.ts b/packages/calling-stateful-client/src/CallContext.ts index 85960a0a899..9a962b7f834 100644 --- a/packages/calling-stateful-client/src/CallContext.ts +++ b/packages/calling-stateful-client/src/CallContext.ts @@ -560,7 +560,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/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index c7ae672093e..ece9392cec2 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, @@ -27,6 +27,7 @@ import { _HighContrastAwareIcon } from './HighContrastAwareIcon'; import { calculateScaledSize, getTogetherModeParticipantOverlayStyle, + participantStatusTransitionStyle, REACTION_MAX_TRAVEL_HEIGHT, REACTION_TRAVEL_HEIGHT, setTogetherModeSeatPositionStyle, @@ -83,13 +84,21 @@ export const TogetherModeOverlay = memo( [key: string]: TogetherModeParticipantStatus; }>({}); const [hoveredParticipantID, setHoveredParticipantID] = useState(''); + const [tabbedParticipantID, setTabbedParticipantID] = useState(''); + + // Reset the Tab key tracking on any other key press + const handleKeyUp = (e: React.KeyboardEvent, 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( @@ -108,7 +117,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) }; @@ -120,22 +134,19 @@ export const TogetherModeOverlay = memo( (id) => !updatedSignals[id] ); - setTogetherModeParticipantStatus((prevSignals) => { - const newSignals = { ...prevSignals, ...updatedSignals }; - const newSignalsLength = Object.keys(newSignals).length; + const newSignals = { ...togetherModeParticipantStatus, ...updatedSignals }; - participantsNotInTogetherModeStream.forEach((id) => { - delete newSignals[id]; - }); + 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(togetherModeParticipantStatus[key]) + ); - return hasChanges ? newSignals : prevSignals; - }); + const updateTogetherModeParticipantStatusState = + hasSignalingChange || Object.keys(newSignals).length !== Object.keys(togetherModeParticipantStatus).length; + return updateTogetherModeParticipantStatusState ? newSignals : togetherModeParticipantStatus; }, [ remoteParticipants, localParticipant, @@ -143,45 +154,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 hasChanges = Object.keys(newSignals).length !== Object.keys(prevSignals).length; - return hasChanges ? 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 && ( @@ -218,7 +222,7 @@ export const TogetherModeOverlay = memo( )} {participantStatus.showDisplayName && ( -
+
{ 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 17a4c6ac3ba..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,112 +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 - ]); - - 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 => { 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/TogetherMode.styles.ts b/packages/react-components/src/components/styles/TogetherMode.styles.ts index b816b7a06b9..864424934f8 100644 --- a/packages/react-components/src/components/styles/TogetherMode.styles.ts +++ b/packages/react-components/src/components/styles/TogetherMode.styles.ts @@ -198,16 +198,25 @@ export const togetherModeParticipantDisplayName = ( isParticipantHovered: boolean, participantSeatingWidth: number, color: string -): CSSProperties => { - 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 +}; diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index 45ade06249e..335e4b0ae25 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -64,6 +64,15 @@ export type { ViewScalingMode } from './types'; +/* @conditional-compile-remove(together-mode) */ +export type { + TogetherModeStreamViewResult, + VideoGalleryTogetherModeParticipantPosition, + VideoGalleryTogetherModeSeatingInfo, + VideoGalleryTogetherModeStreams, + TogetherModeStreamOptions +} from './types'; + export type { RaisedHand } from './types'; export type { Spotlight } from './types'; @@ -87,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-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts b/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts index 8f087ac10ad..0a868752b93 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; @@ -814,7 +814,7 @@ export class AzureCommunicationCallAdapter { return await this.handlers.onCreateTogetherModeStreamView(options); } diff --git a/packages/react-composites/src/composites/CallComposite/selectors/baseSelectors.ts b/packages/react-composites/src/composites/CallComposite/selectors/baseSelectors.ts index b744cb82c81..3dd7f7dd989 100644 --- a/packages/react-composites/src/composites/CallComposite/selectors/baseSelectors.ts +++ b/packages/react-composites/src/composites/CallComposite/selectors/baseSelectors.ts @@ -42,6 +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'; /* @conditional-compile-remove(media-access) */ import { MediaAccess } from '@internal/react-components'; @@ -310,6 +312,24 @@ export const getIsRoomsCall = (state: CallAdapterState): boolean => state.isRoom 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; + +/* @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; /* @conditional-compile-remove(media-access) */ /** @private */ export const getMediaAccessSetting = (state: CallAdapterState): MediaAccess | undefined => diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts index 7f45dbf6783..b4603bf7275 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, @@ -522,7 +522,7 @@ export class AzureCommunicationCallWithChatAdapter implements CallWithChatAdapte } /* @conditional-compile-remove(together-mode) */ public async createTogetherModeStreamView( - options?: VideoStreamOptions + options?: TogetherModeStreamOptions ): Promise { return await this.callAdapter.createTogetherModeStreamView(options); } diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/CallWithChatBackedCallAdapter.ts index 2f0157b1493..23f3cd5c3b5 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, @@ -139,7 +139,7 @@ export class CallWithChatBackedCallAdapter implements CallAdapter { await this.callWithChatAdapter.createStreamView(remoteUserId, options); /* @conditional-compile-remove(together-mode) */ public createTogetherModeStreamView = async ( - options?: VideoStreamOptions + options?: TogetherModeStreamOptions ): Promise => await this.callWithChatAdapter.createTogetherModeStreamView(options); /* @conditional-compile-remove(together-mode) */ diff --git a/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx b/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx index d8a4b18c786..35ecf911142 100644 --- a/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx +++ b/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx @@ -23,6 +23,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, getCapabilites, getLocalUserId } from '../../CallComposite/selectors/baseSelectors'; import { getTeamsMeetingCoordinates, getIsTeamsMeeting } from '../../CallComposite/selectors/baseSelectors'; import { CallControlOptions } from '../../CallComposite'; @@ -68,6 +70,12 @@ 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 participantCapability = useSelector(getCapabilites); + /* @conditional-compile-remove(together-mode) */ + const participantId = useSelector(getLocalUserId); const [dtmfDialerChecked, setDtmfDialerChecked] = useState(props.dtmfDialerPresent ?? false); @@ -336,6 +344,29 @@ 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); + }, + disabled: !( + (participantId?.kind === 'microsoftTeamsUser' && participantCapability?.startTogetherMode?.isPresent) || + isTogetherModeActive + ), + iconProps: { + iconName: 'TogetherModeLayout', + styles: { root: { lineHeight: 0 } } + } + }; + /* @conditional-compile-remove(overflow-top-composite) */ const overflowGalleryOption = { key: 'topKey', @@ -366,6 +397,8 @@ 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) */ + 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/common/Drawer/MoreDrawer.tsx b/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx index 0e3d775800f..7df08f695da 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 { @@ -193,6 +195,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( ( @@ -377,8 +385,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);