From cdc71a00e7bc9e2f6950c2d238cd2129c54086c6 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Thu, 23 Jan 2025 22:28:36 +0000 Subject: [PATCH 01/12] Enable together mode for mobile Enabled together mode signaling divs to be tabable --- .../src/CallContext.ts | 4 +- .../src/components/TogetherModeOverlay.tsx | 84 ++++++------ .../src/components/VideoGallery.tsx | 4 +- .../VideoGallery/TogetherModeLayout.tsx | 129 +----------------- .../VideoGallery/TogetherModeStream.tsx | 1 - .../components/styles/TogetherMode.styles.ts | 41 +++++- packages/react-components/src/index.ts | 18 +-- .../adapter/AzureCommunicationCallAdapter.ts | 4 +- .../CallComposite/selectors/baseSelectors.ts | 20 +++ .../AzureCommunicationCallWithChatAdapter.ts | 4 +- .../adapter/CallWithChatBackedCallAdapter.ts | 4 +- .../common/ControlBar/DesktopMoreButton.tsx | 33 +++++ .../composites/common/Drawer/MoreDrawer.tsx | 29 ++++ 13 files changed, 184 insertions(+), 191 deletions(-) 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/styles/TogetherMode.styles.ts b/packages/react-components/src/components/styles/TogetherMode.styles.ts index b816b7a06b9..941c2d0d39b 100644 --- a/packages/react-components/src/components/styles/TogetherMode.styles.ts +++ b/packages/react-components/src/components/styles/TogetherMode.styles.ts @@ -198,16 +198,26 @@ export const togetherModeParticipantDisplayName = ( isParticipantHovered: boolean, participantSeatingWidth: number, color: string -): CSSProperties => { - const MIN_DISPLAY_NAME_WIDTH = 100; +): React.CSSProperties => { + const width = + isParticipantHovered || participantSeatingWidth * REM_TO_PX_MULTIPLIER > 100 + ? 'fit-content' + : _pxToRem(0.7 * participantSeatingWidth * REM_TO_PX_MULTIPLIER); + + const display = + isParticipantHovered || participantSeatingWidth * REM_TO_PX_MULTIPLIER > 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 * REM_TO_PX_MULTIPLIER) }; }; @@ -221,11 +231,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 }; }; + +/* @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%', + 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); From 75112186f7290abf52fa8e636d217ad14a2269d8 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Thu, 23 Jan 2025 22:32:57 +0000 Subject: [PATCH 02/12] Clean up --- packages/calling-stateful-client/src/CallContext.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/calling-stateful-client/src/CallContext.ts b/packages/calling-stateful-client/src/CallContext.ts index 9a962b7f834..85960a0a899 100644 --- a/packages/calling-stateful-client/src/CallContext.ts +++ b/packages/calling-stateful-client/src/CallContext.ts @@ -560,9 +560,7 @@ export class CallContext { for (const [userId, seatingPosition] of seatingMap.entries()) { seatingPositions[userId] = seatingPosition; } - if (Object.keys(seatingPositions).length > 0) { - call.togetherMode.seatingPositions = seatingPositions; - } + call.togetherMode.seatingPositions = seatingPositions; } }); } From fafedbe136972eb278d726416700b61dbb40dc49 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Thu, 23 Jan 2025 22:33:53 +0000 Subject: [PATCH 03/12] clean up --- packages/react-components/src/components/TogetherModeOverlay.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index ece9392cec2..29eec75f78b 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -179,7 +179,6 @@ export const TogetherModeOverlay = memo( key={participantStatus.id} style={{ ...getTogetherModeParticipantOverlayStyle(participantStatus.seatPositionStyle) - // border: '1px solid yellow' }} onMouseEnter={() => setHoveredParticipantID(participantStatus.id)} onMouseLeave={() => setHoveredParticipantID('')} From 164f5feb9e9ac385ee6a300d2c32ce3230723b22 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Thu, 23 Jan 2025 23:12:05 +0000 Subject: [PATCH 04/12] Clean up --- packages/react-components/src/components/VideoGallery.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-components/src/components/VideoGallery.tsx b/packages/react-components/src/components/VideoGallery.tsx index dbd969ab695..f48525643eb 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -909,6 +909,7 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { /* @conditional-compile-remove(together-mode) */ canSwitchToTogetherModeLayout, layout, layoutProps, + screenShareComponent, screenShareParticipant ]); From bc3d17c5df3176e64f5f42b2e01aee97134cf31b Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Mon, 27 Jan 2025 18:41:54 +0000 Subject: [PATCH 05/12] Addressed comments --- .../src/components/TogetherModeOverlay.tsx | 72 +++++++++---------- .../src/components/VideoGallery.tsx | 13 ++-- .../VideoGallery/TogetherModeLayout.tsx | 5 +- .../components/styles/TogetherMode.styles.ts | 12 ++-- .../CallComposite/selectors/baseSelectors.ts | 4 +- 5 files changed, 50 insertions(+), 56 deletions(-) diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index 29eec75f78b..234c2855596 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -173,7 +173,7 @@ export const TogetherModeOverlay = memo( return (
{Object.values(togetherModeParticipantStatus).map( - (participantStatus, index) => + (participantStatus) => participantStatus.id && (
setHoveredParticipantID('')} onKeyUp={(e) => handleKeyUp(e, participantStatus.id)} onBlur={() => setTabbedParticipantID('')} - tabIndex={index} + tabIndex={0} >
- {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 f48525643eb..2d387ba4e09 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -857,9 +857,7 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { pinnedParticipantUserIds: pinnedParticipants, overflowGalleryPosition, localVideoTileSize, - spotlightedParticipantUserIds: spotlightedParticipants, - /* @conditional-compile-remove(together-mode) */ - togetherModeStreamComponent + spotlightedParticipantUserIds: spotlightedParticipants }), [ remoteParticipants, @@ -877,9 +875,7 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { pinnedParticipants, overflowGalleryPosition, localVideoTileSize, - spotlightedParticipants, - /* @conditional-compile-remove(together-mode) */ - togetherModeStreamComponent + spotlightedParticipants ] ); @@ -902,7 +898,7 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { // 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 (!screenShareComponent && layout === 'togetherMode' && canSwitchToTogetherModeLayout) { - return ; + return ; } return ; }, [ @@ -910,7 +906,8 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { layout, layoutProps, screenShareComponent, - screenShareParticipant + 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 aae61d8ee3d..e2b2eb7b442 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeLayout.tsx @@ -6,8 +6,6 @@ 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 { Stack } from '@fluentui/react'; /* @conditional-compile-remove(together-mode) */ @@ -16,8 +14,7 @@ import { Stack } from '@fluentui/react'; * 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 => { +export const TogetherModeLayout = (props: { togetherModeStreamComponent: JSX.Element }): JSX.Element => { const { togetherModeStreamComponent } = props; - console.log(`TogetherModeLayout: CHUK-1`); return {togetherModeStreamComponent}; }; diff --git a/packages/react-components/src/components/styles/TogetherMode.styles.ts b/packages/react-components/src/components/styles/TogetherMode.styles.ts index 941c2d0d39b..6499941a879 100644 --- a/packages/react-components/src/components/styles/TogetherMode.styles.ts +++ b/packages/react-components/src/components/styles/TogetherMode.styles.ts @@ -199,12 +199,14 @@ export const togetherModeParticipantDisplayName = ( participantSeatingWidth: number, color: string ): 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 > 100 ? 'fit-content' : _pxToRem(0.7 * participantSeatingWidth * REM_TO_PX_MULTIPLIER); - const display = + // 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 > 150 ? 'inline-block' : 'none'; return { @@ -214,7 +216,7 @@ export const togetherModeParticipantDisplayName = ( color, overflow: isParticipantHovered ? 'visible' : 'hidden', width, - display, + display: showDisplayName, fontSize: `${_pxToRem(13)}`, lineHeight: `${_pxToRem(20)}`, maxWidth: isParticipantHovered ? 'fit-content' : _pxToRem(0.7 * participantSeatingWidth * REM_TO_PX_MULTIPLIER) @@ -236,8 +238,7 @@ export const togetherModeParticipantEmojiSpriteStyle = ( width: `${emojiSize}`, position: 'absolute', // Center the emoji sprite within the participant seat - left: `${emojiScaledSizeInPercent / 2}%`, - zIndex: 3 + left: `${emojiScaledSizeInPercent / 2}%` }; }; @@ -253,6 +254,5 @@ export const participantStatusTransitionStyle: CSSProperties = { textAlign: 'center', transform: 'translate(-50%)', transition: 'width 0.3s ease, transform 0.3s ease', - left: '50%', - zIndex: 0 + left: '50%' }; diff --git a/packages/react-composites/src/composites/CallComposite/selectors/baseSelectors.ts b/packages/react-composites/src/composites/CallComposite/selectors/baseSelectors.ts index 3dd7f7dd989..ae0e3281b85 100644 --- a/packages/react-composites/src/composites/CallComposite/selectors/baseSelectors.ts +++ b/packages/react-composites/src/composites/CallComposite/selectors/baseSelectors.ts @@ -325,9 +325,9 @@ export const getIsTogetherModeActive = (state: CallAdapterState): boolean | unde /* @conditional-compile-remove(together-mode) */ /** * @private - * Gets the together mode streams state. + * Gets local participant's user id. * @param state - The current state of the call adapter. - * @returns The together mode streams state or undefined. + * @returns The local participant's user id or undefined. */ export const getLocalUserId = (state: CallAdapterState): CommunicationIdentifierKind | undefined => state.userId; /* @conditional-compile-remove(media-access) */ From 62b4dd37307fa4cb66617465087d0e492e02683c Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Tue, 28 Jan 2025 21:38:23 +0000 Subject: [PATCH 06/12] Addressed comments --- .../src/components/styles/TogetherMode.styles.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/react-components/src/components/styles/TogetherMode.styles.ts b/packages/react-components/src/components/styles/TogetherMode.styles.ts index 6499941a879..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. @@ -201,13 +207,15 @@ export const togetherModeParticipantDisplayName = ( ): 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 > 100 + 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 > 150 ? 'inline-block' : 'none'; + isParticipantHovered || participantSeatingWidth * REM_TO_PX_MULTIPLIER > MAX_DISPLAY_NAME_WIDTH + ? 'inline-block' + : 'none'; return { textOverflow: 'ellipsis', From b4ad0519ea423789a3c0044da1fe4c7e00a39640 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Thu, 30 Jan 2025 16:47:56 +0000 Subject: [PATCH 07/12] Unit test for together mode --- .../components/TogetherModeOverlay.test.tsx | 395 ++++++++++++++++++ .../src/components/TogetherModeOverlay.tsx | 3 +- .../src/components/VideoGallery.test.tsx | 86 ++++ .../src/components/VideoGallery.tsx | 47 ++- .../VideoGallery/TogetherModeStream.tsx | 7 +- 5 files changed, 514 insertions(+), 24 deletions(-) create mode 100644 packages/react-components/src/components/TogetherModeOverlay.test.tsx diff --git a/packages/react-components/src/components/TogetherModeOverlay.test.tsx b/packages/react-components/src/components/TogetherModeOverlay.test.tsx new file mode 100644 index 00000000000..1b1a1b2a2da --- /dev/null +++ b/packages/react-components/src/components/TogetherModeOverlay.test.tsx @@ -0,0 +1,395 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* @conditional-compile-remove(together-mode) */ +import React from 'react'; +/* @conditional-compile-remove(together-mode) */ +import { _ModalClone } from '.'; +/* @conditional-compile-remove(together-mode) */ +import { + ReactionResources, + VideoGalleryLocalParticipant, + VideoGalleryRemoteParticipant, + VideoGalleryTogetherModeParticipantPosition +} from '../types'; +/* @conditional-compile-remove(together-mode) */ +import { v1 as createGUID } from 'uuid'; +/* @conditional-compile-remove(together-mode) */ +import { render } from '@testing-library/react'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeOverlay } from './TogetherModeOverlay'; + +/* @conditional-compile-remove(together-mode) */ +jest.mock('@internal/acs-ui-common', () => { + return { + __esModule: true, + ...jest.requireActual('@internal/acs-ui-common') + }; +}); + +/* @conditional-compile-remove(together-mode) */ +describe('together mode overlay tests', () => { + test('Confirm togetherMode participant Status is not rendered when no participant video stream is available', () => { + const localParticipant = createLocalParticipant({ + videoStream: { isAvailable: false, renderElement: createVideoDivElement() } + }); + const remoteParticipants = Array.from({ length: 10 }, (_, index) => + createRemoteParticipant({ + userId: `remoteParticipant-${index + 1}`, + displayName: `Remote Participant ${index + 1}`, + videoStream: { isAvailable: false, renderElement: createVideoDivElement() } + }) + ); + + const togetherModeOverLayProps = { + emojiSize: 16, + reactionResources: [] as ReactionResources, + localParticipant, + remoteParticipants, + togetherModeSeatPositions: {} + }; + const { container } = render(); + const togetherModeSignalContainers = getTogetherModeSignalContainer(container); + expect(togetherModeSignalContainers.length).toBe(0); + }); + + test('Confirm togetherMode participant Status is not rendered when participant video stream is available but no seating coordinates', () => { + const localParticipant = createLocalParticipant({ + videoStream: { isAvailable: true, renderElement: createVideoDivElement() } + }); + const remoteParticipants = Array.from({ length: 10 }, (_, index) => + createRemoteParticipant({ + userId: `remoteParticipant-${index + 1}`, + displayName: `Remote Participant ${index + 1}`, + videoStream: { isAvailable: true, renderElement: createVideoDivElement() } + }) + ); + + const togetherModeOverLayProps = { + emojiSize: 16, + reactionResources: [] as ReactionResources, + localParticipant, + remoteParticipants, + togetherModeSeatPositions: {} + }; + const { container } = render(); + const togetherModeSignalContainers = getTogetherModeSignalContainer(container); + expect(togetherModeSignalContainers.length).toBe(0); + }); + + test('Confirm togetherMode participant Status is rendered when participant video stream is available and there is seating coordinates', () => { + const localParticipant = createLocalParticipant({ + videoStream: { isAvailable: true, renderElement: createVideoDivElement() } + }); + const remoteParticipants = Array.from({ length: 10 }, (_, index) => + createRemoteParticipant({ + userId: `remoteParticipant-${index + 1}`, + displayName: `Remote Participant ${index + 1}`, + videoStream: { isAvailable: true, renderElement: createVideoDivElement() } + }) + ); + + const togetherModeSeatPositions: VideoGalleryTogetherModeParticipantPosition = { + [localParticipant.userId]: { top: 0, left: 0, width: 0, height: 0 }, + ...Object.fromEntries( + remoteParticipants.map((participant, index) => [ + participant.userId, + { top: index * 10, left: index * 10, width: 100, height: 100 } + ]) + ) + }; + const togetherModeOverLayProps = { + emojiSize: 16, + reactionResources: [] as ReactionResources, + localParticipant, + remoteParticipants, + togetherModeSeatPositions + }; + const { container } = render(); + const togetherModeSignalContainers = getTogetherModeSignalContainer(container); + expect(togetherModeSignalContainers.length).toBe(11); + }); + + test('Confirm displayName is rendered when hand is raised', () => { + // const raisedHand: RaisedHand = { + // raisedHandOrderPosition: 1 + // }; + const localParticipant = createLocalParticipant({ + videoStream: { isAvailable: true, renderElement: createVideoDivElement() } + }); + const remoteParticipants = Array.from({ length: 1 }, (_, index) => + createRemoteParticipant({ + userId: `remoteParticipant-${index + 1}`, + displayName: `Remote Participant ${index + 1}`, + videoStream: { isAvailable: true, renderElement: createVideoDivElement() } + }) + ); + + const togetherModeSeatPositions: VideoGalleryTogetherModeParticipantPosition = { + [localParticipant.userId]: { top: 0, left: 0, width: 0, height: 0 }, + ...Object.fromEntries( + remoteParticipants.map((participant, index) => [ + participant.userId, + { top: index * 10, left: index * 10, width: 100, height: 100 } + ]) + ) + }; + const togetherModeOverLayProps = { + emojiSize: 16, + reactionResources: [] as ReactionResources, + localParticipant, + remoteParticipants, + togetherModeSeatPositions + }; + const { container } = render(); + const togetherModeSignalContainers = getTogetherModeSignalContainer(container); + expect(togetherModeSignalContainers.length).toBe(2); + }); + + test('Confirm displayName is rendered when participants perform reactions', () => { + const localParticipant = createLocalParticipant({ + videoStream: { isAvailable: true, renderElement: createVideoDivElement() } + }); + const remoteParticipants = Array.from({ length: 1 }, (_, index) => + createRemoteParticipant({ + userId: `remoteParticipant-${index + 1}`, + displayName: `Remote Participant ${index + 1}`, + videoStream: { isAvailable: true, renderElement: createVideoDivElement() } + }) + ); + + const togetherModeSeatPositions: VideoGalleryTogetherModeParticipantPosition = { + [localParticipant.userId]: { top: 0, left: 0, width: 0, height: 0 }, + ...Object.fromEntries( + remoteParticipants.map((participant, index) => [ + participant.userId, + { top: index * 10, left: index * 10, width: 100, height: 100 } + ]) + ) + }; + const togetherModeOverLayProps = { + emojiSize: 16, + reactionResources: [] as ReactionResources, + localParticipant, + remoteParticipants, + togetherModeSeatPositions + }; + const { rerender, container } = render(); + + let renderedReactions = getTogetherModeReactions(container); + expect(renderedReactions.length).toBe(0); + + const updatedLocalParticipant = { + ...localParticipant, + reaction: { reactionType: '👍', receivedOn: new Date() } + }; + + const updatedRemoteParticipants = remoteParticipants.map((participant) => ({ + ...participant, + reaction: { reactionType: '❤️', receivedOn: new Date() } + })); + + rerender( + + ); + renderedReactions = getTogetherModeReactions(container); + expect(renderedReactions.length).toBe(2); + }); + + test('Confirm displayName is rendered when participants hands are raised', () => { + const localParticipant = createLocalParticipant({ + videoStream: { isAvailable: true, renderElement: createVideoDivElement() } + }); + const remoteParticipants = Array.from({ length: 1 }, (_, index) => + createRemoteParticipant({ + userId: `remoteParticipant-${index + 1}`, + displayName: `Remote Participant ${index + 1}`, + videoStream: { isAvailable: true, renderElement: createVideoDivElement() } + }) + ); + + const participantsDisplayNames = [localParticipant.displayName, remoteParticipants[0]?.displayName ?? '']; + + const togetherModeSeatPositions: VideoGalleryTogetherModeParticipantPosition = { + [localParticipant.userId]: { top: 0, left: 0, width: 0, height: 0 }, + ...Object.fromEntries( + remoteParticipants.map((participant, index) => [ + participant.userId, + { top: index * 10, left: index * 10, width: 100, height: 100 } + ]) + ) + }; + const togetherModeOverLayProps = { + emojiSize: 16, + reactionResources: [] as ReactionResources, + localParticipant, + remoteParticipants, + togetherModeSeatPositions + }; + const { rerender, container } = render(); + + let renderedDisplayNames = getParticipantDisplayName(container); + expect(renderedDisplayNames.length).toBe(0); + + const updatedLocalParticipant = { + ...localParticipant, + spotlight: { spotlightedOrderPosition: 1 } + }; + + const updatedRemoteParticipants = remoteParticipants.map((participant, index) => ({ + ...participant, + spotlight: { spotlightedOrderPosition: index + 1 } + })); + + rerender( + + ); + renderedDisplayNames = getParticipantDisplayName(container); + expect(renderedDisplayNames.length).toBe(2); + expect(renderedDisplayNames.sort()).toEqual(participantsDisplayNames.sort()); + }); + + test('Confirm displayName is rendered when participants are spotlighted', () => { + const localParticipant = createLocalParticipant({ + videoStream: { isAvailable: true, renderElement: createVideoDivElement() } + }); + const remoteParticipants = Array.from({ length: 1 }, (_, index) => + createRemoteParticipant({ + userId: `remoteParticipant-${index + 1}`, + displayName: `Remote Participant ${index + 1}`, + videoStream: { isAvailable: true, renderElement: createVideoDivElement() } + }) + ); + + const participantsDisplayNames = [localParticipant.displayName, remoteParticipants[0]?.displayName ?? '']; + + const togetherModeSeatPositions: VideoGalleryTogetherModeParticipantPosition = { + [localParticipant.userId]: { top: 0, left: 0, width: 0, height: 0 }, + ...Object.fromEntries( + remoteParticipants.map((participant, index) => [ + participant.userId, + { top: index * 10, left: index * 10, width: 100, height: 100 } + ]) + ) + }; + const togetherModeOverLayProps = { + emojiSize: 16, + reactionResources: [] as ReactionResources, + localParticipant, + remoteParticipants, + togetherModeSeatPositions + }; + const { rerender, container } = render(); + + let renderedDisplayNames = getParticipantDisplayName(container); + expect(renderedDisplayNames.length).toBe(0); + + const updatedLocalParticipant = { + ...localParticipant, + spotlight: { spotlightedOrderPosition: 1 } + }; + + const updatedRemoteParticipants = remoteParticipants.map((participant, index) => ({ + ...participant, + spotlight: { spotlightedOrderPosition: index + 1 } + })); + + rerender( + + ); + renderedDisplayNames = getParticipantDisplayName(container); + expect(renderedDisplayNames.length).toBe(2); + expect(renderedDisplayNames.sort()).toEqual(participantsDisplayNames.sort()); + }); +}); + +/* @conditional-compile-remove(together-mode) */ +const getTogetherModeSignalContainer = (root: Element | null): Element[] => + Array.from(root?.querySelectorAll('[data-ui-group="together-mode-participant"]') ?? []); + +/* @conditional-compile-remove(together-mode) */ +const getTogetherModeReactions = (root: Element | null): Element[] => + Array.from(root?.querySelectorAll('[data-ui-group="together-mode-participant-reaction"]') ?? []); + +/* @conditional-compile-remove(together-mode) */ +const getParticipantDisplayName = (root: Element | null): string[] => { + const participantSignalingStatus = getTogetherModeSignalContainer(root); + const renderedDisplayNames: string[] = []; + + participantSignalingStatus?.forEach((status) => { + const span = status.querySelector('span'); // Get the first span inside the div + if (span) { + if (span.textContent) { + renderedDisplayNames.push(span.textContent.trim()); // Store the span text + } + } + }); + + return renderedDisplayNames; +}; + +/* @conditional-compile-remove(together-mode) */ +const createLocalParticipant = (attrs?: Partial): VideoGalleryLocalParticipant => { + return { + userId: attrs?.userId ?? 'localParticipant', + isMuted: attrs?.isMuted ?? false, + displayName: attrs?.displayName ?? 'Local Participant', + isScreenSharingOn: attrs?.isScreenSharingOn ?? false, + raisedHand: attrs?.raisedHand ?? undefined, + videoStream: { + id: attrs?.videoStream?.id ?? Math.random(), + isAvailable: attrs?.videoStream?.isAvailable ?? false, + isReceiving: attrs?.videoStream?.isReceiving ?? true, + isMirrored: attrs?.videoStream?.isMirrored ?? false, + renderElement: attrs?.videoStream?.renderElement ?? undefined + } + }; +}; + +/* @conditional-compile-remove(together-mode) */ +const createVideoDivElement = (): HTMLDivElement => { + const divElement = document.createElement('div'); + divElement.innerHTML = ''; + return divElement; +}; + +/* @conditional-compile-remove(together-mode) */ +const createRemoteParticipant = (attrs?: Partial): VideoGalleryRemoteParticipant => { + return { + userId: attrs?.userId ?? `remoteParticipant-${createGUID()}`, + displayName: attrs?.displayName ?? 'Remote Participant', + isMuted: attrs?.isMuted ?? false, + isSpeaking: attrs?.isSpeaking ?? false, + /* @conditional-compile-remove(demo) */ state: attrs?.state ?? 'Connected', + raisedHand: attrs?.raisedHand ?? undefined, + screenShareStream: { + id: attrs?.screenShareStream?.id ?? 1, + isAvailable: attrs?.screenShareStream?.isAvailable ?? false, + isReceiving: attrs?.screenShareStream?.isReceiving ?? true, + isMirrored: attrs?.screenShareStream?.isMirrored ?? false, + renderElement: attrs?.screenShareStream?.renderElement ?? undefined + }, + videoStream: { + id: attrs?.videoStream?.id ?? 1, + isAvailable: attrs?.videoStream?.isAvailable ?? false, + isReceiving: attrs?.videoStream?.isReceiving ?? true, + isMirrored: attrs?.videoStream?.isMirrored ?? false, + renderElement: attrs?.videoStream?.renderElement ?? undefined, + scalingMode: attrs?.videoStream?.scalingMode ?? 'Crop' + }, + isScreenSharingOn: attrs?.isScreenSharingOn ?? false + }; +}; diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index 234c2855596..4645caaeb50 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -104,7 +104,6 @@ export const TogetherModeOverlay = memo( const participantsWithVideoAvailable = allParticipants.filter( (p) => p.videoStream?.isAvailable && togetherModeSeatPositions[p.userId] ); - const updatedSignals: { [key: string]: TogetherModeParticipantStatus } = {}; for (const p of participantsWithVideoAvailable) { const { userId, reaction, raisedHand, spotlight, isMuted, displayName } = p; @@ -177,6 +176,7 @@ export const TogetherModeOverlay = memo( participantStatus.id && (
{ + const createTestProps = (overrides: Partial = {}, userId: string): VideoGalleryProps => ({ + layout: 'togetherMode', + localParticipant: createLocalParticipant({ + userId, + videoStream: { isAvailable: false, renderElement: createVideoDivElement() } + }), + remoteParticipants: [ + createRemoteParticipant({ + videoStream: { isAvailable: false, renderElement: createVideoDivElement() } + }) + ], + ...overrides + }); + + const runTogetherModeTests = (userId: string, canStartTogetherMode: boolean) => { + test('Confirm Together Mode Layout state when capability IS Present', () => { + const { container } = render(); + // Together Mode capability is present will always be false for ACS users, the test here is to confirm + // if for some reason the capability is present becomes true for ACS users, the canSwitchToTogetherModeLayout value + // remains false + canStartTogetherMode + ? expect(getTogetherModeLayout(container)).toBeTruthy() + : expect(getTogetherModeLayout(container)).toBeFalsy(); + }); + + test('Confirm Together Mode CANNOT be started when capability NOT Present', () => { + const { container } = render(); + expect(getTogetherModeLayout(container)).toBeFalsy(); + }); + + test('Confirm Together Mode Layout CAN be switched when together mode is already active', () => { + const { container } = render(); + expect(getTogetherModeLayout(container)).toBeTruthy(); + }); + + test('Confirm Together Mode Layout CANNOT be switched when together mode is NOT active', () => { + const { container } = render(); + expect(getTogetherModeLayout(container)).toBeFalsy(); + }); + + test('Confirm Together Mode View is NOT active when screenSharing is active', () => { + const props = createTestProps({ isTogetherModeActive: true }, userId); + const { rerender, container } = render(); + expect(getTogetherModeLayout(container)).toBeTruthy(); + + props.localParticipant.isScreenSharingOn = true; + props.localParticipant.screenShareStream = { + isAvailable: true, + renderElement: createRemoteScreenShareVideoDivElement() + }; + rerender(); + expect(getTogetherModeLayout(container)).toBeFalsy(); + }); + + test('Confirm Together Mode View is active when screenSharing is stopped', () => { + const props = createTestProps({ isTogetherModeActive: true }, userId); + const { rerender, container } = render(); + expect(getTogetherModeLayout(container)).toBeTruthy(); + + props.localParticipant.isScreenSharingOn = true; + props.localParticipant.screenShareStream = { + isAvailable: true, + renderElement: createRemoteScreenShareVideoDivElement() + }; + rerender(); + expect(getTogetherModeLayout(container)).toBeFalsy(); + + props.localParticipant.isScreenSharingOn = false; + rerender(); + expect(getTogetherModeLayout(container)).toBeTruthy(); + }); + }; + + describe('Local Participant is a Teams User', () => { + runTogetherModeTests('8:orgid:localParticipant', true); + }); + + describe('Local Participant is an ACS User', () => { + runTogetherModeTests('8:acs:localParticipant', false); + }); +}); + const getFloatingLocalVideoModal = (root: Element | null): Element | null => root?.querySelector('[data-ui-id="floating-local-video-host"]') ?? null; const getLocalVideoTile = (root: Element | null): Element | null => @@ -803,6 +886,9 @@ const getHorizontalGallery = (root: Element | null): Element | null => const getVerticalGallery = (root: Element | null): Element | null => root?.querySelector('[data-ui-id="responsive-vertical-gallery"]') ?? null; +const getTogetherModeLayout = (root: Element | null): Element | null => + root?.querySelector('[data-ui-id="together-mode-layout"]') ?? null; + const getTiles = (root: Element | null): Element[] => Array.from(root?.querySelectorAll('[data-ui-id="video-tile"]') ?? []); const getGridTiles = (root: Element | null): Element[] => Array.from(getTiles(getGridLayout(root))); diff --git a/packages/react-components/src/components/VideoGallery.tsx b/packages/react-components/src/components/VideoGallery.tsx index 2d387ba4e09..f4d17667fda 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -475,7 +475,6 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { /* @conditional-compile-remove(media-access) */ onPermitVideo } = props; - const ids = useIdentifiers(); const theme = useTheme(); const localeStrings = useLocale().strings.videoGallery; @@ -801,8 +800,12 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { : undefined; /* @conditional-compile-remove(together-mode) */ - const togetherModeStreamComponent = useMemo( - () => ( + const togetherModeStreamComponent = useMemo(() => { + if (layout !== 'togetherMode' || screenShareComponent) { + return null; + } + + return ( { containerWidth={containerWidth} containerHeight={containerHeight} /> - ), - [ - startTogetherModeEnabled, - isTogetherModeActive, - onCreateTogetherModeStreamView, - onStartTogetherMode, - onDisposeTogetherModeStreamView, - onSetTogetherModeSceneSize, - togetherModeStreams, - togetherModeSeatingCoordinates, - localParticipant, - remoteParticipants, - reactionResources, - containerWidth, - containerHeight - ] - ); + ); + }, [ + layout, + screenShareComponent, + startTogetherModeEnabled, + isTogetherModeActive, + onCreateTogetherModeStreamView, + onStartTogetherMode, + onDisposeTogetherModeStreamView, + onSetTogetherModeSceneSize, + togetherModeStreams, + togetherModeSeatingCoordinates, + localParticipant, + remoteParticipants, + reactionResources, + containerWidth, + 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. @@ -897,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 (!screenShareComponent && layout === 'togetherMode' && canSwitchToTogetherModeLayout) { + if (layout === 'togetherMode' && togetherModeStreamComponent && canSwitchToTogetherModeLayout) { return ; } return ; @@ -905,7 +909,6 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { /* @conditional-compile-remove(together-mode) */ canSwitchToTogetherModeLayout, layout, layoutProps, - screenShareComponent, screenShareParticipant, /* @conditional-compile-remove(together-mode) */ togetherModeStreamComponent ]); diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx index d81639b1112..3b6d5cddc26 100644 --- a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx +++ b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx @@ -89,7 +89,12 @@ export const TogetherModeStream = memo( const showLoadingIndicator = !(stream && stream.isAvailable && stream.isReceiving); return containerWidth && containerHeight ? ( - + Date: Fri, 31 Jan 2025 18:14:55 +0000 Subject: [PATCH 08/12] revert to main changes --- .../src/components/VideoGallery.tsx | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/packages/react-components/src/components/VideoGallery.tsx b/packages/react-components/src/components/VideoGallery.tsx index 0bd43d71591..4caa52575e3 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -467,6 +467,7 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { onForbidVideo, onPermitVideo } = props; + const ids = useIdentifiers(); const theme = useTheme(); const localeStrings = useLocale().strings.videoGallery; @@ -783,12 +784,8 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { : undefined; /* @conditional-compile-remove(together-mode) */ - const togetherModeStreamComponent = useMemo(() => { - if (layout !== 'togetherMode' || screenShareComponent) { - return null; - } - - return ( + const togetherModeStreamComponent = useMemo( + () => ( { containerWidth={containerWidth} containerHeight={containerHeight} /> - ); - }, [ - layout, - screenShareComponent, - startTogetherModeEnabled, - isTogetherModeActive, - onCreateTogetherModeStreamView, - onStartTogetherMode, - onDisposeTogetherModeStreamView, - onSetTogetherModeSceneSize, - togetherModeStreams, - togetherModeSeatingCoordinates, - localParticipant, - remoteParticipants, - reactionResources, - containerWidth, - containerHeight - ]); + ), + [ + startTogetherModeEnabled, + isTogetherModeActive, + onCreateTogetherModeStreamView, + onStartTogetherMode, + onDisposeTogetherModeStreamView, + onSetTogetherModeSceneSize, + togetherModeStreams, + togetherModeSeatingCoordinates, + localParticipant, + remoteParticipants, + reactionResources, + containerWidth, + 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. @@ -884,7 +880,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' && togetherModeStreamComponent && canSwitchToTogetherModeLayout) { + if (!screenShareComponent && layout === 'togetherMode' && canSwitchToTogetherModeLayout) { return ; } return ; @@ -892,6 +888,7 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { /* @conditional-compile-remove(together-mode) */ canSwitchToTogetherModeLayout, layout, layoutProps, + screenShareComponent, screenShareParticipant, /* @conditional-compile-remove(together-mode) */ togetherModeStreamComponent ]); From 8511d6550e0a820a0fa44ad99eb9c2d96fedb79b Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Thu, 6 Feb 2025 16:09:46 +0000 Subject: [PATCH 09/12] Addressed comments --- .../components/TogetherModeOverlay.test.tsx | 24 ++++++++++------- .../src/components/TogetherModeOverlay.tsx | 26 ++++--------------- 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/packages/react-components/src/components/TogetherModeOverlay.test.tsx b/packages/react-components/src/components/TogetherModeOverlay.test.tsx index 1b1a1b2a2da..4a7ce3cc405 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.test.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.test.tsx @@ -111,17 +111,16 @@ describe('together mode overlay tests', () => { }); test('Confirm displayName is rendered when hand is raised', () => { - // const raisedHand: RaisedHand = { - // raisedHandOrderPosition: 1 - // }; const localParticipant = createLocalParticipant({ - videoStream: { isAvailable: true, renderElement: createVideoDivElement() } + videoStream: { isAvailable: true, renderElement: createVideoDivElement() }, + raisedHand: { raisedHandOrderPosition: 1 } }); const remoteParticipants = Array.from({ length: 1 }, (_, index) => createRemoteParticipant({ userId: `remoteParticipant-${index + 1}`, displayName: `Remote Participant ${index + 1}`, - videoStream: { isAvailable: true, renderElement: createVideoDivElement() } + videoStream: { isAvailable: true, renderElement: createVideoDivElement() }, + raisedHand: { raisedHandOrderPosition: index + 1 } }) ); @@ -179,14 +178,18 @@ describe('together mode overlay tests', () => { let renderedReactions = getTogetherModeReactions(container); expect(renderedReactions.length).toBe(0); + enum ReactionType { + like = '👍', + heart = '❤️' + } const updatedLocalParticipant = { ...localParticipant, - reaction: { reactionType: '👍', receivedOn: new Date() } + reaction: { reactionType: ReactionType.like, receivedOn: new Date() } }; const updatedRemoteParticipants = remoteParticipants.map((participant) => ({ ...participant, - reaction: { reactionType: '❤️', receivedOn: new Date() } + reaction: { reactionType: ReactionType.heart, receivedOn: new Date() } })); rerender( @@ -317,12 +320,13 @@ describe('together mode overlay tests', () => { }); /* @conditional-compile-remove(together-mode) */ -const getTogetherModeSignalContainer = (root: Element | null): Element[] => - Array.from(root?.querySelectorAll('[data-ui-group="together-mode-participant"]') ?? []); +const getTogetherModeSignalContainer = (root: Element | null): Element[] => { + return Array.from(root?.querySelectorAll('[data-ui-id^="together-mode-participant"]') ?? []); +}; /* @conditional-compile-remove(together-mode) */ const getTogetherModeReactions = (root: Element | null): Element[] => - Array.from(root?.querySelectorAll('[data-ui-group="together-mode-participant-reaction"]') ?? []); + Array.from(root?.querySelectorAll('[data-ui-id^="together-mode-participant-reaction"]') ?? []); /* @conditional-compile-remove(together-mode) */ const getParticipantDisplayName = (root: Element | null): string[] => { diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx index 5883a25a7c6..2ee44ecc342 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -84,14 +84,6 @@ 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. @@ -104,6 +96,7 @@ export const TogetherModeOverlay = memo( const participantsWithVideoAvailable = allParticipants.filter( (p) => p.videoStream?.isAvailable && togetherModeSeatPositions[p.userId] ); + const updatedSignals: { [key: string]: TogetherModeParticipantStatus } = {}; for (const p of participantsWithVideoAvailable) { const { userId, reaction, raisedHand, spotlight, isMuted, displayName } = p; @@ -116,12 +109,7 @@ export const TogetherModeOverlay = memo( isSpotlighted: !!spotlight, isMuted, displayName: displayName || locale.strings.videoGallery.displayNamePlaceholder, - showDisplayName: !!( - spotlight || - raisedHand || - hoveredParticipantID === userId || - tabbedParticipantID === userId - ), + showDisplayName: !!(spotlight || raisedHand || hoveredParticipantID === userId), scaledSize: calculateScaledSize(seatingPosition.width, seatingPosition.height), seatPositionStyle: setTogetherModeSeatPositionStyle(seatingPosition) }; @@ -153,8 +141,7 @@ export const TogetherModeOverlay = memo( togetherModeSeatPositions, reactionResources, locale.strings.videoGallery.displayNamePlaceholder, - hoveredParticipantID, - tabbedParticipantID + hoveredParticipantID ]); useEffect(() => { @@ -172,15 +159,12 @@ export const TogetherModeOverlay = memo( participantStatus.id && (
setHoveredParticipantID(participantStatus.id)} onMouseLeave={() => setHoveredParticipantID('')} - onKeyUp={(e) => handleKeyUp(e, participantStatus.id)} - onBlur={() => setTabbedParticipantID('')} - tabIndex={0} >
{participantStatus.showDisplayName && ( @@ -226,7 +210,6 @@ export const TogetherModeOverlay = memo( // 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
Date: Thu, 6 Feb 2025 18:53:02 +0000 Subject: [PATCH 10/12] fixed compilation issue --- packages/react-components/src/components/VideoGallery.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-components/src/components/VideoGallery.test.tsx b/packages/react-components/src/components/VideoGallery.test.tsx index dc7c7a125ed..eb3bafe1cf4 100644 --- a/packages/react-components/src/components/VideoGallery.test.tsx +++ b/packages/react-components/src/components/VideoGallery.test.tsx @@ -791,6 +791,7 @@ test('should render screenshare component and local user video tile when local u expect(tileIsVideo(localVideoTile)).toBe(true); }); +/* @conditional-compile-remove(together-mode) */ describe('VideoGallery together mode layout tests', () => { const createTestProps = (overrides: Partial = {}, userId: string): VideoGalleryProps => ({ layout: 'togetherMode', @@ -886,6 +887,7 @@ const getHorizontalGallery = (root: Element | null): Element | null => const getVerticalGallery = (root: Element | null): Element | null => root?.querySelector('[data-ui-id="responsive-vertical-gallery"]') ?? null; +/* @conditional-compile-remove(together-mode) */ const getTogetherModeLayout = (root: Element | null): Element | null => root?.querySelector('[data-ui-id="together-mode-layout"]') ?? null; From 020c5202f815f46cbeae275b090283d66be8f77d Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Fri, 7 Feb 2025 19:59:42 +0000 Subject: [PATCH 11/12] fix lint issue --- packages/react-components/src/components/VideoGallery.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/src/components/VideoGallery.test.tsx b/packages/react-components/src/components/VideoGallery.test.tsx index eb3bafe1cf4..65c4d9c0f7c 100644 --- a/packages/react-components/src/components/VideoGallery.test.tsx +++ b/packages/react-components/src/components/VideoGallery.test.tsx @@ -807,7 +807,7 @@ describe('VideoGallery together mode layout tests', () => { ...overrides }); - const runTogetherModeTests = (userId: string, canStartTogetherMode: boolean) => { + const runTogetherModeTests = (userId: string, canStartTogetherMode: boolean): void => { test('Confirm Together Mode Layout state when capability IS Present', () => { const { container } = render(); // Together Mode capability is present will always be false for ACS users, the test here is to confirm From 65dabad41b7e993740f30c9528f62554435f2551 Mon Sep 17 00:00:00 2001 From: Chukwuebuka Nwankwo Date: Mon, 10 Feb 2025 17:46:57 +0000 Subject: [PATCH 12/12] Included dummy test --- .../src/components/TogetherModeOverlay.test.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/react-components/src/components/TogetherModeOverlay.test.tsx b/packages/react-components/src/components/TogetherModeOverlay.test.tsx index 4a7ce3cc405..bfecdc36cf1 100644 --- a/packages/react-components/src/components/TogetherModeOverlay.test.tsx +++ b/packages/react-components/src/components/TogetherModeOverlay.test.tsx @@ -27,8 +27,18 @@ jest.mock('@internal/acs-ui-common', () => { }; }); +describe('Dummy Test', () => { + test('Dummy test to ensure test suite is not empty', () => { + expect(true).toBe(true); + }); +}); + /* @conditional-compile-remove(together-mode) */ describe('together mode overlay tests', () => { + test('Dummy test to ensure test suite is not empty', () => { + expect(true).toBe(true); + }); + test('Confirm togetherMode participant Status is not rendered when no participant video stream is available', () => { const localParticipant = createLocalParticipant({ videoStream: { isAvailable: false, renderElement: createVideoDivElement() }