diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index c7ae672093e..b9e7448d0e7 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, @@ -89,7 +90,7 @@ export const TogetherModeOverlay = memo( * 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( @@ -120,22 +121,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, @@ -146,29 +144,13 @@ export const TogetherModeOverlay = memo( hoveredParticipantID ]); - /* - * 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] - ); - - setTogetherModeParticipantStatus((prevSignals) => { - const newSignals = { ...prevSignals }; - removedVisibleParticipants.forEach((participantId) => { - delete newSignals[participantId]; - }); + useEffect(() => { + if (hoveredParticipantID && !updatedParticipantStatus[hoveredParticipantID]) { + setHoveredParticipantID(''); + } - // 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, updatedParticipantStatus]); return (
@@ -184,41 +166,8 @@ export const TogetherModeOverlay = memo( onMouseLeave={() => setHoveredParticipantID('')} >
- {participantStatus.reaction?.reactionType && ( - // First div - Section that fixes the travel height and applies the movement animation - // Second div - Responsible for ensuring the sprite emoji is always centered in the participant seat position - // Third div - Play Animation as the other animation applies on the base play animation for the sprite -
-
-
-
-
- )} - {participantStatus.showDisplayName && ( -
+
)} + + {participantStatus.reaction?.reactionType && ( + // First div - Section that fixes the travel height and applies the movement animation + // Second div - Responsible for ensuring the sprite emoji is always centered in the participant seat position + // Third div - Play Animation as the other animation applies on the base play animation for the sprite +
+
+
+
+
+ )}
) diff --git a/packages/react-components/src/components/VideoGallery.tsx b/packages/react-components/src/components/VideoGallery.tsx index 951538f2837..4caa52575e3 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -798,7 +798,6 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { localParticipant={localParticipant} remoteParticipants={remoteParticipants} reactionResources={reactionResources} - screenShareComponent={screenShareComponent} containerWidth={containerWidth} containerHeight={containerHeight} /> @@ -815,7 +814,6 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { localParticipant, remoteParticipants, reactionResources, - screenShareComponent, containerWidth, containerHeight ] @@ -842,9 +840,7 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { pinnedParticipantUserIds: pinnedParticipants, overflowGalleryPosition, localVideoTileSize, - spotlightedParticipantUserIds: spotlightedParticipants, - /* @conditional-compile-remove(together-mode) */ - togetherModeStreamComponent + spotlightedParticipantUserIds: spotlightedParticipants }), [ remoteParticipants, @@ -862,9 +858,7 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { pinnedParticipants, overflowGalleryPosition, localVideoTileSize, - spotlightedParticipants, - /* @conditional-compile-remove(together-mode) */ - togetherModeStreamComponent + spotlightedParticipants ] ); @@ -886,15 +880,17 @@ 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) { - return ; + if (!screenShareComponent && layout === 'togetherMode' && canSwitchToTogetherModeLayout) { + return ; } return ; }, [ /* @conditional-compile-remove(together-mode) */ canSwitchToTogetherModeLayout, layout, layoutProps, - screenShareParticipant + screenShareComponent, + screenShareParticipant, + /* @conditional-compile-remove(together-mode) */ togetherModeStreamComponent ]); return ( diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx index 17a4c6ac3ba..e2b2eb7b442 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx @@ -2,27 +2,11 @@ // 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) */ /** @@ -30,113 +14,7 @@ import { videoGalleryLayoutGap } from './styles/Layout.styles'; * 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 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; +export const TogetherModeLayout = (props: { togetherModeStreamComponent: JSX.Element }): JSX.Element => { + const { togetherModeStreamComponent } = props; + 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/styles/TogetherMode.styles.ts b/packages/react-components/src/components/styles/TogetherMode.styles.ts index b816b7a06b9..3bd8d260684 100644 --- a/packages/react-components/src/components/styles/TogetherMode.styles.ts +++ b/packages/react-components/src/components/styles/TogetherMode.styles.ts @@ -30,6 +30,12 @@ export const REACTION_TRAVEL_HEIGHT = 0.35 * REM_TO_PX_MULTIPLIER; */ export const REACTION_MAX_TRAVEL_HEIGHT = 0.5 * REM_TO_PX_MULTIPLIER; +/* @conditional-compile-remove(together-mode) */ +/** + * The maximum width for displaying the participant's display name. + */ +export const MAX_DISPLAY_NAME_WIDTH = 150; + /* @conditional-compile-remove(together-mode) */ /** * Interface for defining the coordinates of a seat in Together Mode. @@ -198,16 +204,30 @@ export const togetherModeParticipantDisplayName = ( isParticipantHovered: boolean, participantSeatingWidth: number, color: string -): CSSProperties => { - const MIN_DISPLAY_NAME_WIDTH = 100; +): React.CSSProperties => { + // expands the display name width when participant is hovered or clicked on else make it 70% of the participant seating width + const width = + isParticipantHovered || participantSeatingWidth * REM_TO_PX_MULTIPLIER > MAX_DISPLAY_NAME_WIDTH + ? 'fit-content' + : _pxToRem(0.7 * participantSeatingWidth * REM_TO_PX_MULTIPLIER); + + // For smaller displays, the display name is hidden only participant is hovered or clicked on for mobile view + const showDisplayName = + isParticipantHovered || participantSeatingWidth * REM_TO_PX_MULTIPLIER > MAX_DISPLAY_NAME_WIDTH + ? '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: showDisplayName, + fontSize: `${_pxToRem(13)}`, + lineHeight: `${_pxToRem(20)}`, + maxWidth: isParticipantHovered ? 'fit-content' : _pxToRem(0.7 * participantSeatingWidth * REM_TO_PX_MULTIPLIER) }; }; @@ -221,7 +241,7 @@ 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', @@ -229,3 +249,18 @@ export const togetherModeParticipantEmojiSpriteStyle = ( left: `${emojiScaledSizeInPercent / 2}%` }; }; + +/* @conditional-compile-remove(together-mode) */ +/** + * 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', + transform: 'translate(-50%)', + transition: 'width 0.3s ease, transform 0.3s ease', + left: '50%' +}; 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 c0f80e64a9a..561db28f6e3 100644 --- a/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts @@ -121,7 +121,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; @@ -820,7 +820,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 44041c63c99..84a2c55bc14 100644 --- a/packages/react-composites/src/composites/CallComposite/selectors/baseSelectors.ts +++ b/packages/react-composites/src/composites/CallComposite/selectors/baseSelectors.ts @@ -44,6 +44,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'; @@ -332,6 +334,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 local participant's user id. + * @param state - The current state of the call adapter. + * @returns The local participant's user id 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 86048758c13..c5fe973421f 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, @@ -525,7 +525,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 d335f2ed8b4..58dc06b2709 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 9c74d4944c3..8a7b347c8f2 100644 --- a/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx +++ b/packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx @@ -25,6 +25,13 @@ 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, + getIsTeamsCall +} from '../../CallComposite/selectors/baseSelectors'; import { getTeamsMeetingCoordinates, getIsTeamsMeeting } from '../../CallComposite/selectors/baseSelectors'; import { CallControlOptions } from '../../CallComposite'; @@ -78,6 +85,14 @@ 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); + /* @conditional-compile-remove(together-mode) */ + const isTeamsCall = useSelector(getIsTeamsCall); const [dtmfDialerChecked, setDtmfDialerChecked] = useState(props.dtmfDialerPresent ?? false); @@ -394,6 +409,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', @@ -424,6 +462,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 (isTeamsCall || isTeamsMeeting) { + 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 1668a5b62df..75431f45eba 100644 --- a/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx +++ b/packages/react-composites/src/composites/common/Drawer/MoreDrawer.tsx @@ -45,6 +45,13 @@ 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, + getIsTeamsCall +} from '../../CallComposite/selectors/baseSelectors'; /** @private */ export interface MoreDrawerStrings { @@ -203,7 +210,16 @@ 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); + /* @conditional-compile-remove(together-mode) */ + const isTeamsCall = useSelector(getIsTeamsCall); + const isTeamsMeeting = getIsTeamsMeeting(callAdapter.getState()); const onSpeakerItemClick = useCallback( ( _ev: React.MouseEvent | React.KeyboardEvent | undefined, @@ -389,8 +405,31 @@ 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) */ + if (isTeamsCall || isTeamsMeeting) { + galleryLayoutOptions.subMenuProps?.push(togetherModeOption); + } if (drawerSelectionOptions !== false && isEnabled(drawerSelectionOptions?.galleryControlsButton)) { drawerMenuItems.push(galleryLayoutOptions); @@ -448,8 +487,6 @@ export const MoreDrawer = (props: MoreDrawerProps): JSX.Element => { }); } - const isTeamsMeeting = getIsTeamsMeeting(callAdapter.getState()); - const teamsMeetingCoordinates = getTeamsMeetingCoordinates(callAdapter.getState()); if ( diff --git a/packages/react-composites/tests/browser/snapshots/beta/tests/browser/call/hermetic/VideoGallery.test.ts-snapshots/default-layout-Desktop-Chrome-linux.png b/packages/react-composites/tests/browser/snapshots/beta/tests/browser/call/hermetic/VideoGallery.test.ts-snapshots/default-layout-Desktop-Chrome-linux.png index c6d4839f524..f3924505285 100644 Binary files a/packages/react-composites/tests/browser/snapshots/beta/tests/browser/call/hermetic/VideoGallery.test.ts-snapshots/default-layout-Desktop-Chrome-linux.png and b/packages/react-composites/tests/browser/snapshots/beta/tests/browser/call/hermetic/VideoGallery.test.ts-snapshots/default-layout-Desktop-Chrome-linux.png differ diff --git a/packages/react-composites/tests/browser/snapshots/beta/tests/browser/call/hermetic/VideoGallery.test.ts-snapshots/gallery-options-mobile-Mobile-Android-Portrait-linux.png b/packages/react-composites/tests/browser/snapshots/beta/tests/browser/call/hermetic/VideoGallery.test.ts-snapshots/gallery-options-mobile-Mobile-Android-Portrait-linux.png index c208d9f46c4..0897ddafc6f 100644 Binary files a/packages/react-composites/tests/browser/snapshots/beta/tests/browser/call/hermetic/VideoGallery.test.ts-snapshots/gallery-options-mobile-Mobile-Android-Portrait-linux.png and b/packages/react-composites/tests/browser/snapshots/beta/tests/browser/call/hermetic/VideoGallery.test.ts-snapshots/gallery-options-mobile-Mobile-Android-Portrait-linux.png differ