Skip to content

Commit

Permalink
Full screen captions banner api (#5164)
Browse files Browse the repository at this point in the history
* 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 3e1ea7c.

* remove test code

* Update packages/react-components/src/components/CaptionsBanner.tsx

Co-authored-by: edwardlee-msft <[email protected]>
Signed-off-by: Porter Nan <[email protected]>

* Update CaptionsBanner.tsx

Signed-off-by: Porter Nan <[email protected]>

* Update api view

* Fix linting problem

---------

Signed-off-by: Porter Nan <[email protected]>
Co-authored-by: edwardlee-msft <[email protected]>
  • Loading branch information
PorterNan and edwardlee-msft authored Sep 12, 2024
1 parent db232bb commit 870d010
Show file tree
Hide file tree
Showing 12 changed files with 141 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "minor",
"area": "improvement",
"workstream": "Full screen captions",
"comment": "Caption fullscreen mode api",
"packageName": "@azure/communication-react",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "minor",
"area": "improvement",
"workstream": "Full screen captions",
"comment": "Caption fullscreen mode api",
"packageName": "@azure/communication-react",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,9 @@ export type CallCompositeIcons = {

// @public
export type CallCompositeOptions = {
captionsBanner?: {
height: 'full' | 'default';
};
errorBar?: boolean;
callControls?: boolean | CallControlOptions;
deviceChecks?: DeviceCheckOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,9 @@ export type CallCompositeIcons = {

// @public
export type CallCompositeOptions = {
captionsBanner?: {
height: 'full' | 'default';
};
errorBar?: boolean;
callControls?: boolean | CallControlOptions;
remoteVideoTileMenuOptions?: RemoteVideoTileMenuOptions;
Expand Down
44 changes: 38 additions & 6 deletions packages/react-components/src/components/CaptionsBanner.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<HTMLDivElement>(null);
const [isAtBottomOfScroll, setIsAtBottomOfScroll] = useState<boolean>(true);
const theme = useTheme();

const scrollToBottom = (): void => {
if (captionsScrollDivRef.current) {
Expand All @@ -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);
}, []);
Expand All @@ -97,9 +113,17 @@ export const _CaptionsBanner = (props: _CaptionsBannerProps): JSX.Element => {
return (
<>
{startCaptionsInProgress && (
<FocusZone as="ul" className={captionsContainerClassName}>
<FocusZone as="ul" className={captionsContainerClassName} data-ui-id="captions-banner">
{isCaptionsOn && (
<div ref={captionsScrollDivRef} className={captionsBannerClassName(formFactor)}>
<div
ref={captionsScrollDivRef}
className={
captionsOptions?.height === 'full'
? captionsBannerFullHeightClassName(theme)
: captionsBannerClassName(formFactor)
}
data-ui-id="captions-banner-inner"
>
{captions.map((caption) => {
return (
<div key={caption.id} className={captionContainerClassName} data-is-focusable={true}>
Expand All @@ -110,7 +134,15 @@ export const _CaptionsBanner = (props: _CaptionsBannerProps): JSX.Element => {
</div>
)}
{!isCaptionsOn && (
<Stack verticalAlign="center" styles={loadingBannerStyles(formFactor)} data-is-focusable={true}>
<Stack
verticalAlign="center"
styles={
captionsOptions?.height === 'full'
? loadingBannerFullHeightStyles(theme)
: loadingBannerStyles(formFactor)
}
data-is-focusable={true}
>
<Spinner label={strings?.captionsBannerSpinnerText} ariaLive="assertive" labelPosition="right" />
</Stack>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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
*/
Expand All @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ export interface CallArrangementProps {
pinnedParticipants?: string[];
setPinnedParticipants?: (pinnedParticipants: string[]) => void;
doNotShowCameraAccessNotifications?: boolean;
captionsOptions?: {
height: 'full' | 'default';
};
}

/**
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -583,7 +593,7 @@ export const CallArrangement = (props: CallArrangementProps): JSX.Element => {
<Stack horizontal grow>
<Stack.Item style={callCompositeContainerCSS}>
<Stack.Item styles={callGalleryStyles} grow>
<Stack verticalFill styles={mediaGalleryContainerStyles}>
<Stack verticalFill styles={galleryContainerStyles}>
<Stack.Item styles={notificationsContainerStyles}>
{
/* @conditional-compile-remove(breakout-rooms) */
Expand Down Expand Up @@ -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 && (
<CaptionsBanner
captionsOptions={props.captionsOptions}
isMobile={props.mobileView}
onFetchAvatarPersonaData={props.onFetchAvatarPersonaData}
useTeamsCaptions={useTeamsCaptions}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export interface MediaGalleryProps {
setPromptProps: (props: PromptProps) => void;
hideSpotlightButtons?: boolean;
videoTilesOptions?: VideoTilesOptions;
captionsOptions?: {
height: 'full' | 'default';
};
}

/**
Expand All @@ -78,7 +81,8 @@ export const MediaGallery = (props: MediaGalleryProps): JSX.Element => {
setIsPromptOpen,
setPromptProps,
hideSpotlightButtons,
videoTilesOptions
videoTilesOptions,
captionsOptions
} = props;

const videoGalleryProps = usePropsFor(VideoGallery);
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -222,27 +230,28 @@ export const MediaGallery = (props: MediaGalleryProps): JSX.Element => {
);
}, [
videoGalleryProps,
videoTilesOptions,
galleryStyles,
props.isMobile,
props.localVideoTileOptions,
props.userSetGalleryLayout,
cameraSwitcherProps,
onRenderAvatar,
remoteVideoTileMenuOptions,
overflowGalleryPosition,
userRole,
isRoomsCall,
containerAspectRatio,
props.userSetGalleryLayout,
pinnedParticipants,
onPinParticipant,
onUnpinParticipant,
layoutBasedOnTilePosition,
reactionResources,
hideSpotlightButtons,
onStartLocalSpotlightWithPrompt,
onStopLocalSpotlightWithPrompt,
onStartRemoteSpotlightWithPrompt,
onStopRemoteSpotlightWithPrompt,
hideSpotlightButtons,
videoTilesOptions
layoutBasedOnTilePosition
]);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export const CallPage = (props: CallPageProps): JSX.Element => {
setPromptProps={setPromptProps}
hideSpotlightButtons={options?.spotlight?.hideSpotlightButtons}
videoTilesOptions={options?.videoTilesOptions}
captionsOptions={options?.captionsBanner}
/>
);
}
Expand Down Expand Up @@ -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}
/>
{<Prompt isOpen={isPromptOpen} onDismiss={() => setIsPromptOpen(false)} {...promptProps} />}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ export const galleryParentContainerStyles = (backgroundColor: string): IStackSty
*/
export const mediaGalleryContainerStyles: IStackItemStyles = {
root: {
height: '100%'
height: '100%',
width: '100%'
}
};

Expand Down
16 changes: 13 additions & 3 deletions packages/react-composites/src/composites/common/CaptionsBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export const CaptionsBanner = (props: {
isMobile: boolean;
useTeamsCaptions?: boolean;
onFetchAvatarPersonaData?: AvatarPersonaDataCallback;
captionsOptions?: {
height: 'full' | 'default';
};
}): JSX.Element => {
const captionsBannerProps = useAdaptedSelector(_captionsBannerSelector);

Expand All @@ -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',
Expand Down Expand Up @@ -90,6 +99,7 @@ export const CaptionsBanner = (props: {
<_CaptionsBanner
{...captionsBannerProps}
{...handlers}
captionsOptions={props.captionsOptions}
onRenderAvatar={onRenderAvatar}
formFactor={props.isMobile ? 'compact' : 'default'}
strings={captionsBannerStrings}
Expand Down

0 comments on commit 870d010

Please sign in to comment.