From 870d010092b413c466575aaa19bba43c292ca884 Mon Sep 17 00:00:00 2001 From: Porter Nan Date: Wed, 11 Sep 2024 17:23:44 -0700 Subject: [PATCH] Full screen captions banner api (#5164) * Add extra bundle for enable video effect * hide video gallery and display caption only * Caption fullscreen mode api * Revert "Add extra bundle for enable video effect" This reverts commit 3e1ea7c59f6ce09960e81d18f932516a3190621d. * remove test code * Update packages/react-components/src/components/CaptionsBanner.tsx Co-authored-by: edwardlee-msft Signed-off-by: Porter Nan * Update CaptionsBanner.tsx Signed-off-by: Porter Nan * Update api view * Fix linting problem --------- Signed-off-by: Porter Nan Co-authored-by: edwardlee-msft --- ...-a8b252ce-eb1d-4b71-b06f-b47055cea16b.json | 9 ++++ ...-a8b252ce-eb1d-4b71-b06f-b47055cea16b.json | 9 ++++ .../review/beta/communication-react.api.md | 3 ++ .../review/stable/communication-react.api.md | 3 ++ .../src/components/CaptionsBanner.tsx | 44 ++++++++++++++++--- .../src/components/styles/Captions.style.ts | 33 +++++++++++++- .../CallComposite/CallComposite.tsx | 3 ++ .../components/CallArrangement.tsx | 13 +++++- .../CallComposite/components/MediaGallery.tsx | 21 ++++++--- .../CallComposite/pages/CallPage.tsx | 2 + .../CallComposite/styles/CallPage.styles.ts | 3 +- .../src/composites/common/CaptionsBanner.tsx | 16 +++++-- 12 files changed, 141 insertions(+), 18 deletions(-) create mode 100644 change-beta/@azure-communication-react-a8b252ce-eb1d-4b71-b06f-b47055cea16b.json create mode 100644 change/@azure-communication-react-a8b252ce-eb1d-4b71-b06f-b47055cea16b.json diff --git a/change-beta/@azure-communication-react-a8b252ce-eb1d-4b71-b06f-b47055cea16b.json b/change-beta/@azure-communication-react-a8b252ce-eb1d-4b71-b06f-b47055cea16b.json new file mode 100644 index 00000000000..04673b39fda --- /dev/null +++ b/change-beta/@azure-communication-react-a8b252ce-eb1d-4b71-b06f-b47055cea16b.json @@ -0,0 +1,9 @@ +{ + "type": "minor", + "area": "improvement", + "workstream": "Full screen captions", + "comment": "Caption fullscreen mode api", + "packageName": "@azure/communication-react", + "email": "jiangnanhello@live.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-communication-react-a8b252ce-eb1d-4b71-b06f-b47055cea16b.json b/change/@azure-communication-react-a8b252ce-eb1d-4b71-b06f-b47055cea16b.json new file mode 100644 index 00000000000..04673b39fda --- /dev/null +++ b/change/@azure-communication-react-a8b252ce-eb1d-4b71-b06f-b47055cea16b.json @@ -0,0 +1,9 @@ +{ + "type": "minor", + "area": "improvement", + "workstream": "Full screen captions", + "comment": "Caption fullscreen mode api", + "packageName": "@azure/communication-react", + "email": "jiangnanhello@live.com", + "dependentChangeType": "patch" +} diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index c96d846f87c..9549628907b 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -728,6 +728,9 @@ export type CallCompositeIcons = { // @public export type CallCompositeOptions = { + captionsBanner?: { + height: 'full' | 'default'; + }; errorBar?: boolean; callControls?: boolean | CallControlOptions; deviceChecks?: DeviceCheckOptions; diff --git a/packages/communication-react/review/stable/communication-react.api.md b/packages/communication-react/review/stable/communication-react.api.md index 2d3d92fae0b..929fc258181 100644 --- a/packages/communication-react/review/stable/communication-react.api.md +++ b/packages/communication-react/review/stable/communication-react.api.md @@ -537,6 +537,9 @@ export type CallCompositeIcons = { // @public export type CallCompositeOptions = { + captionsBanner?: { + height: 'full' | 'default'; + }; errorBar?: boolean; callControls?: boolean | CallControlOptions; remoteVideoTileMenuOptions?: RemoteVideoTileMenuOptions; diff --git a/packages/react-components/src/components/CaptionsBanner.tsx b/packages/react-components/src/components/CaptionsBanner.tsx index 82f8ece459b..1d6f85efc8b 100644 --- a/packages/react-components/src/components/CaptionsBanner.tsx +++ b/packages/react-components/src/components/CaptionsBanner.tsx @@ -1,12 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { Stack, FocusZone, Spinner } from '@fluentui/react'; +import { Stack, FocusZone, Spinner, useTheme } from '@fluentui/react'; import React, { useEffect, useRef, useState, useCallback } from 'react'; import { _Caption } from './Caption'; import { captionContainerClassName, captionsBannerClassName, + captionsBannerFullHeightClassName, captionsContainerClassName, + loadingBannerFullHeightStyles, loadingBannerStyles } from './styles/Captions.style'; import { OnRenderAvatarCallback } from '../types'; @@ -50,16 +52,30 @@ export interface _CaptionsBannerProps { * @defaultValue 'default' */ formFactor?: 'default' | 'compact'; + captionsOptions?: { + height: 'full' | 'default'; + }; } +const SCROLL_OFFSET_ALLOWANCE = 20; + /** * @internal * A component for displaying a CaptionsBanner with user icon, displayName and captions text. */ export const _CaptionsBanner = (props: _CaptionsBannerProps): JSX.Element => { - const { captions, isCaptionsOn, startCaptionsInProgress, onRenderAvatar, strings, formFactor = 'default' } = props; + const { + captions, + isCaptionsOn, + startCaptionsInProgress, + onRenderAvatar, + strings, + formFactor = 'default', + captionsOptions + } = props; const captionsScrollDivRef = useRef(null); const [isAtBottomOfScroll, setIsAtBottomOfScroll] = useState(true); + const theme = useTheme(); const scrollToBottom = (): void => { if (captionsScrollDivRef.current) { @@ -73,7 +89,7 @@ export const _CaptionsBanner = (props: _CaptionsBannerProps): JSX.Element => { } const atBottom = Math.ceil(captionsScrollDivRef.current.scrollTop) >= - captionsScrollDivRef.current.scrollHeight - captionsScrollDivRef.current.clientHeight; + captionsScrollDivRef.current.scrollHeight - captionsScrollDivRef.current.clientHeight - SCROLL_OFFSET_ALLOWANCE; setIsAtBottomOfScroll(atBottom); }, []); @@ -97,9 +113,17 @@ export const _CaptionsBanner = (props: _CaptionsBannerProps): JSX.Element => { return ( <> {startCaptionsInProgress && ( - + {isCaptionsOn && ( -
+
{captions.map((caption) => { return (
@@ -110,7 +134,15 @@ export const _CaptionsBanner = (props: _CaptionsBannerProps): JSX.Element => {
)} {!isCaptionsOn && ( - + )} diff --git a/packages/react-components/src/components/styles/Captions.style.ts b/packages/react-components/src/components/styles/Captions.style.ts index 12a7b737b30..3e9522f1e13 100644 --- a/packages/react-components/src/components/styles/Captions.style.ts +++ b/packages/react-components/src/components/styles/Captions.style.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { IStackStyles, mergeStyles } from '@fluentui/react'; +import { IStackStyles, ITheme, mergeStyles } from '@fluentui/react'; import { _pxToRem } from '@internal/acs-ui-common'; import { scrollbarStyles } from './Common.style'; @@ -62,6 +62,22 @@ export const captionsBannerClassName = (formFactor: 'default' | 'compact'): stri }); }; +/** + * @private + */ +export const captionsBannerFullHeightClassName = (theme: ITheme): string => { + return mergeStyles({ + overflowX: 'hidden', + overflowY: 'auto', + height: '100%', + width: '100%', + position: 'absolute', + backgroundColor: theme.palette.white, + left: 0, + ...scrollbarStyles + }); +}; + /** * @private */ @@ -73,6 +89,21 @@ export const loadingBannerStyles = (formFactor: 'default' | 'compact'): IStackSt }; }; +/** + * @private + */ +export const loadingBannerFullHeightStyles = (theme: ITheme): IStackStyles => { + return { + root: { + height: '100%', + width: '100%', + position: 'absolute', + left: 0, + backgroundColor: theme.palette.white + } + }; +}; + /** * @private */ diff --git a/packages/react-composites/src/composites/CallComposite/CallComposite.tsx b/packages/react-composites/src/composites/CallComposite/CallComposite.tsx index 4cd750c9629..43a4de8e1f2 100644 --- a/packages/react-composites/src/composites/CallComposite/CallComposite.tsx +++ b/packages/react-composites/src/composites/CallComposite/CallComposite.tsx @@ -153,6 +153,9 @@ export interface LocalVideoTileOptions { * @public */ export type CallCompositeOptions = { + captionsBanner?: { + height: 'full' | 'default'; + }; /** * Surface Azure Communication Services backend errors in the UI with {@link @azure/communication-react#ErrorBar}. * Hide or show the error bar. diff --git a/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx b/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx index 293e607f5ad..f3d13796f13 100644 --- a/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx +++ b/packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx @@ -135,6 +135,9 @@ export interface CallArrangementProps { pinnedParticipants?: string[]; setPinnedParticipants?: (pinnedParticipants: string[]) => void; doNotShowCameraAccessNotifications?: boolean; + captionsOptions?: { + height: 'full' | 'default'; + }; } /** @@ -462,6 +465,13 @@ export const CallArrangement = (props: CallArrangementProps): JSX.Element => { const minMaxDragPosition = useMinMaxDragPosition(props.modalLayerHostId); const pipStyles = useMemo(() => getPipStyles(theme), [theme]); + const galleryContainerStyles = useMemo(() => { + return { + ...mediaGalleryContainerStyles, + ...(props?.captionsOptions?.height === 'full' ? { root: { postion: 'absolute' } } : {}) + }; + }, [props?.captionsOptions?.height]); + if (isTeamsMeeting) { filteredLatestErrorNotifications .filter((notification) => notification.type === 'teamsMeetingCallNetworkQualityLow') @@ -583,7 +593,7 @@ export const CallArrangement = (props: CallArrangementProps): JSX.Element => { - + { /* @conditional-compile-remove(breakout-rooms) */ @@ -621,6 +631,7 @@ export const CallArrangement = (props: CallArrangementProps): JSX.Element => { {true && /* @conditional-compile-remove(PSTN-calls) */ /* @conditional-compile-remove(one-to-n-calling) */ !isInLocalHold && ( void; hideSpotlightButtons?: boolean; videoTilesOptions?: VideoTilesOptions; + captionsOptions?: { + height: 'full' | 'default'; + }; } /** @@ -78,7 +81,8 @@ export const MediaGallery = (props: MediaGalleryProps): JSX.Element => { setIsPromptOpen, setPromptProps, hideSpotlightButtons, - videoTilesOptions + videoTilesOptions, + captionsOptions } = props; const videoGalleryProps = usePropsFor(VideoGallery); @@ -160,6 +164,10 @@ export const MediaGallery = (props: MediaGalleryProps): JSX.Element => { setPromptProps ); + const galleryStyles = useMemo(() => { + return { ...VideoGalleryStyles, ...(captionsOptions?.height === 'full' ? { root: { postion: 'absolute' } } : {}) }; + }, [captionsOptions?.height]); + const onPinParticipant = useMemo(() => { return setPinnedParticipants ? (userId: string) => { @@ -190,7 +198,7 @@ export const MediaGallery = (props: MediaGalleryProps): JSX.Element => { videoTilesOptions={videoTilesOptions} localVideoViewOptions={localVideoViewOptions} remoteVideoViewOptions={remoteVideoViewOptions} - styles={VideoGalleryStyles} + styles={galleryStyles} layout={layoutBasedOnUserSelection()} showCameraSwitcherInLocalPreview={props.isMobile} localVideoCameraCycleButtonProps={cameraSwitcherProps} @@ -222,8 +230,11 @@ export const MediaGallery = (props: MediaGalleryProps): JSX.Element => { ); }, [ videoGalleryProps, + videoTilesOptions, + galleryStyles, props.isMobile, props.localVideoTileOptions, + props.userSetGalleryLayout, cameraSwitcherProps, onRenderAvatar, remoteVideoTileMenuOptions, @@ -231,18 +242,16 @@ export const MediaGallery = (props: MediaGalleryProps): JSX.Element => { userRole, isRoomsCall, containerAspectRatio, - props.userSetGalleryLayout, pinnedParticipants, onPinParticipant, onUnpinParticipant, - layoutBasedOnTilePosition, reactionResources, + hideSpotlightButtons, onStartLocalSpotlightWithPrompt, onStopLocalSpotlightWithPrompt, onStartRemoteSpotlightWithPrompt, onStopRemoteSpotlightWithPrompt, - hideSpotlightButtons, - videoTilesOptions + layoutBasedOnTilePosition ]); return ( diff --git a/packages/react-composites/src/composites/CallComposite/pages/CallPage.tsx b/packages/react-composites/src/composites/CallComposite/pages/CallPage.tsx index dbb951a8251..b8b0c7213ce 100644 --- a/packages/react-composites/src/composites/CallComposite/pages/CallPage.tsx +++ b/packages/react-composites/src/composites/CallComposite/pages/CallPage.tsx @@ -157,6 +157,7 @@ export const CallPage = (props: CallPageProps): JSX.Element => { setPromptProps={setPromptProps} hideSpotlightButtons={options?.spotlight?.hideSpotlightButtons} videoTilesOptions={options?.videoTilesOptions} + captionsOptions={options?.captionsBanner} /> ); } @@ -211,6 +212,7 @@ export const CallPage = (props: CallPageProps): JSX.Element => { setPinnedParticipants={setPinnedParticipants} /* @conditional-compile-remove(call-readiness) */ doNotShowCameraAccessNotifications={props.options?.deviceChecks?.camera === 'doNotPrompt'} + captionsOptions={options?.captionsBanner} /> { setIsPromptOpen(false)} {...promptProps} />} diff --git a/packages/react-composites/src/composites/CallComposite/styles/CallPage.styles.ts b/packages/react-composites/src/composites/CallComposite/styles/CallPage.styles.ts index 1cb9854bf42..3622b279d92 100644 --- a/packages/react-composites/src/composites/CallComposite/styles/CallPage.styles.ts +++ b/packages/react-composites/src/composites/CallComposite/styles/CallPage.styles.ts @@ -59,7 +59,8 @@ export const galleryParentContainerStyles = (backgroundColor: string): IStackSty */ export const mediaGalleryContainerStyles: IStackItemStyles = { root: { - height: '100%' + height: '100%', + width: '100%' } }; diff --git a/packages/react-composites/src/composites/common/CaptionsBanner.tsx b/packages/react-composites/src/composites/common/CaptionsBanner.tsx index c9cb2a8ac1e..8ec786fbc27 100644 --- a/packages/react-composites/src/composites/common/CaptionsBanner.tsx +++ b/packages/react-composites/src/composites/common/CaptionsBanner.tsx @@ -21,6 +21,9 @@ export const CaptionsBanner = (props: { isMobile: boolean; useTeamsCaptions?: boolean; onFetchAvatarPersonaData?: AvatarPersonaDataCallback; + captionsOptions?: { + height: 'full' | 'default'; + }; }): JSX.Element => { const captionsBannerProps = useAdaptedSelector(_captionsBannerSelector); @@ -36,9 +39,15 @@ export const CaptionsBanner = (props: { setIsCaptionsSettingsOpen(false); }; - const containerClassName = mergeStyles({ - position: 'relative' - }); + const containerClassName = mergeStyles( + props.captionsOptions?.height === 'full' + ? mergeStyles({ + position: 'absolute', + height: '100%', + width: '100%' + }) + : { position: 'relative' } + ); const floatingChildClassName = mergeStyles({ position: 'absolute', @@ -90,6 +99,7 @@ export const CaptionsBanner = (props: { <_CaptionsBanner {...captionsBannerProps} {...handlers} + captionsOptions={props.captionsOptions} onRenderAvatar={onRenderAvatar} formFactor={props.isMobile ? 'compact' : 'default'} strings={captionsBannerStrings}