packages/react-composites/src/composites/common/ControlBar/DesktopMoreButton.tsx (460 lines of code) (raw):

// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { IContextualMenuItem } from '@fluentui/react'; import { ControlBarButtonProps } from '@internal/react-components'; import { CaptionsBanner } from '@internal/react-components'; import { VideoGalleryLayout } from '@internal/react-components'; import { HoldButton } from '@internal/react-components'; import { StartCaptionsButton } from '@internal/react-components'; import React from 'react'; import { useState } from 'react'; import { useMemo, useCallback } from 'react'; import { usePropsFor } from '../../CallComposite/hooks/usePropsFor'; import { buttonFlyoutIncreasedSizeStyles } from '../../CallComposite/styles/Buttons.styles'; import { MoreButton } from '../MoreButton'; import { useLocale } from '../../localization'; import { CommonCallControlOptions } from '../types/CommonCallControlOptions'; import { CUSTOM_BUTTON_OPTIONS, generateCustomCallDesktopOverflowButtons, onFetchCustomButtonPropsTrampoline } from './CustomButton'; import { _preventDismissOnEvent } from '@internal/acs-ui-common'; import { showDtmfDialer } from '../../CallComposite/utils/MediaGalleryUtils'; import { useSelector } from '../../CallComposite/hooks/useSelector'; import { getTargetCallees } from '../../CallComposite/selectors/baseSelectors'; /* @conditional-compile-remove(together-mode) */ import { getIsTogetherModeActive, getCapabilites, getLocalUserId, getIsTeamsCall } from '../../CallComposite/selectors/baseSelectors'; import { getTeamsMeetingCoordinates, getIsTeamsMeeting } from '../../CallComposite/selectors/baseSelectors'; import { CallControlOptions } from '../../CallComposite'; /** @private */ export interface DesktopMoreButtonProps extends ControlBarButtonProps { disableButtonsForHoldScreen?: boolean; onClickShowDialpad?: () => void; isCaptionsSupported?: boolean; isRealTimeTextSupported?: boolean; callControls?: boolean | CommonCallControlOptions; onCaptionsSettingsClick?: () => void; onStartRealTimeTextClick?: () => void; startRealTimeTextButtonChecked?: boolean; onUserSetOverflowGalleryPositionChange?: (position: 'Responsive' | 'horizontalTop') => void; onUserSetGalleryLayout?: (layout: VideoGalleryLayout) => void; userSetGalleryLayout?: VideoGalleryLayout; onSetDialpadPage?: () => void; dtmfDialerPresent?: boolean; teamsMeetingPhoneCallEnable?: boolean; onMeetingPhoneInfoClick?: () => void; } /** * * @private */ export const DesktopMoreButton = (props: DesktopMoreButtonProps): JSX.Element => { const localeStrings = useLocale(); const holdButtonProps = usePropsFor(HoldButton); const startCaptionsButtonProps = usePropsFor(StartCaptionsButton); const realTimeTextProps = usePropsFor(CaptionsBanner); const startCaptions = useCallback(async () => { await startCaptionsButtonProps.onStartCaptions({ spokenLanguage: startCaptionsButtonProps.currentSpokenLanguage }); }, [startCaptionsButtonProps]); /* @conditional-compile-remove(overflow-top-composite) */ const [galleryPositionTop, setGalleryPositionTop] = useState<boolean>(false); const [focusedContentOn, setFocusedContentOn] = useState<boolean>(false); const [previousLayout, setPreviousLayout] = useState<VideoGalleryLayout>( props.userSetGalleryLayout ?? 'floatingLocalVideo' ); const callees = useSelector(getTargetCallees); const allowDtmfDialer = showDtmfDialer(callees); const isTeamsMeeting = useSelector(getIsTeamsMeeting); const teamsMeetingCoordinates = useSelector(getTeamsMeetingCoordinates); /* @conditional-compile-remove(together-mode) */ const isTogetherModeActive = useSelector(getIsTogetherModeActive); /* @conditional-compile-remove(together-mode) */ const participantCapability = useSelector(getCapabilites); /* @conditional-compile-remove(together-mode) */ const participantId = useSelector(getLocalUserId); /* @conditional-compile-remove(together-mode) */ const isTeamsCall = useSelector(getIsTeamsCall); const [dtmfDialerChecked, setDtmfDialerChecked] = useState<boolean>(props.dtmfDialerPresent ?? false); const moreButtonStrings = useMemo( () => ({ label: localeStrings.strings.call.moreButtonCallingLabel, tooltipOffContent: localeStrings.strings.callWithChat.moreDrawerButtonTooltip }), [localeStrings] ); const moreButtonContextualMenuItems: IContextualMenuItem[] = []; const menuSubIconStyleSet = { root: { height: 'unset', lineHeight: '100%', width: '1.25rem' } }; if (props.callControls === true || (props.callControls as CallControlOptions)?.holdButton !== false) { moreButtonContextualMenuItems.push({ key: 'holdButtonKey', text: localeStrings.component.strings.holdButton.tooltipOffContent, onClick: () => { holdButtonProps.onToggleHold(); }, iconProps: { iconName: 'HoldCallContextualMenuItem', styles: { root: { lineHeight: 0 } } }, itemProps: { styles: buttonFlyoutIncreasedSizeStyles }, disabled: props.disableButtonsForHoldScreen }); } // is captions feature is active if (props.isCaptionsSupported) { const captionsContextualMenuItems: IContextualMenuItem[] = []; moreButtonContextualMenuItems.push({ key: 'liveCaptionsKey', id: 'common-call-composite-captions-button', text: localeStrings.strings.call.liveCaptionsLabel, iconProps: { iconName: 'CaptionsIcon', styles: { root: { lineHeight: 0 } } }, itemProps: { styles: buttonFlyoutIncreasedSizeStyles }, disabled: props.disableButtonsForHoldScreen, subMenuProps: { id: 'captions-contextual-menu', items: captionsContextualMenuItems, calloutProps: { preventDismissOnEvent: _preventDismissOnEvent } }, submenuIconProps: { iconName: 'HorizontalGalleryRightButton', styles: menuSubIconStyleSet } }); captionsContextualMenuItems.push({ key: 'ToggleCaptionsKey', id: 'common-call-composite-captions-toggle-button', text: startCaptionsButtonProps.checked ? localeStrings.strings.call.startCaptionsButtonTooltipOnContent : localeStrings.strings.call.startCaptionsButtonTooltipOffContent, onClick: () => { startCaptionsButtonProps.checked ? startCaptionsButtonProps.onStopCaptions() : startCaptionsButtonProps.currentSpokenLanguage !== '' ? startCaptions() : props.onCaptionsSettingsClick && props.onCaptionsSettingsClick(); }, iconProps: { iconName: startCaptionsButtonProps.checked ? 'CaptionsOffIcon' : 'CaptionsIcon', styles: { root: { lineHeight: 0 } } }, itemProps: { styles: buttonFlyoutIncreasedSizeStyles }, disabled: props.disableButtonsForHoldScreen }); if (props.onCaptionsSettingsClick) { captionsContextualMenuItems.push({ key: 'openCaptionsSettingsKey', id: 'common-call-composite-captions-settings-button', text: localeStrings.strings.call.captionsSettingsLabel, onClick: props.onCaptionsSettingsClick, iconProps: { iconName: 'CaptionsSettingsIcon', styles: { root: { lineHeight: 0 } } }, itemProps: { styles: buttonFlyoutIncreasedSizeStyles }, disabled: props.disableButtonsForHoldScreen || !startCaptionsButtonProps.checked }); } } //RTT if (props.isRealTimeTextSupported) { const realTimeTextContextualMenuItems: IContextualMenuItem[] = []; const rttDisabled = props.disableButtonsForHoldScreen || realTimeTextProps.isRealTimeTextOn || props.startRealTimeTextButtonChecked; moreButtonContextualMenuItems.push({ key: 'realTimeTextKey', id: 'common-call-composite-rtt-button', text: localeStrings.strings.call.realTimeTextLabel, iconProps: { iconName: 'RealTimeTextIcon', styles: { root: { lineHeight: 0 } } }, itemProps: { styles: buttonFlyoutIncreasedSizeStyles }, disabled: props.disableButtonsForHoldScreen, subMenuProps: { id: 'rtt-contextual-menu', items: realTimeTextContextualMenuItems, calloutProps: { preventDismissOnEvent: _preventDismissOnEvent } }, submenuIconProps: { iconName: 'HorizontalGalleryRightButton', styles: menuSubIconStyleSet } }); realTimeTextContextualMenuItems.push({ key: 'StartRealTimeTextKey', id: 'common-call-composite-rtt-start-button', text: localeStrings.strings.call.startRealTimeTextLabel, ariaLabel: rttDisabled ? localeStrings.strings.call.disabledStartRealTimeTextLabel : localeStrings.strings.call.startRealTimeTextLabel, onClick: props.onStartRealTimeTextClick, iconProps: { iconName: 'RealTimeTextIcon', styles: { root: { lineHeight: 0 } } }, itemProps: { styles: buttonFlyoutIncreasedSizeStyles }, disabled: rttDisabled }); } const dtmfDialerScreenOption = { key: 'dtmfDialerScreenKey', itemProps: { styles: buttonFlyoutIncreasedSizeStyles }, text: !dtmfDialerChecked ? localeStrings.strings.call.dtmfDialerMoreButtonLabelOn : localeStrings.strings.call.dtmfDialerMoreButtonLabelOff, onClick: () => { props.onSetDialpadPage && props.onSetDialpadPage(); setDtmfDialerChecked(!dtmfDialerChecked); }, iconProps: { iconName: 'DtmfDialpadButton', styles: { root: { lineHeight: 0 } } } }; /** * Only render the dtmf dialer if the dialpad for PSTN calls is not present */ if (props.onSetDialpadPage && allowDtmfDialer) { if (props.callControls === true || (props.callControls as CallControlOptions)?.dtmfDialerButton !== false) { moreButtonContextualMenuItems.push(dtmfDialerScreenOption); } } const joinByPhoneOption = { key: 'phoneCallKey', itemProps: { styles: buttonFlyoutIncreasedSizeStyles }, text: localeStrings.strings.call.phoneCallMoreButtonLabel, onClick: () => { props.onMeetingPhoneInfoClick && props.onMeetingPhoneInfoClick(); }, iconProps: { iconName: 'PhoneNumberButton', styles: { root: { lineHeight: 0 } } } }; /** * Only render the phone call button if meeting conordinates are present */ if (props.teamsMeetingPhoneCallEnable && isTeamsMeeting && teamsMeetingCoordinates) { moreButtonContextualMenuItems.push(joinByPhoneOption); } if (props.onUserSetOverflowGalleryPositionChange) { const galleryOptions = { key: 'overflowGalleryPositionKey', iconProps: { iconName: 'GalleryOptions', styles: { root: { lineHeight: 0 } } }, itemProps: { styles: buttonFlyoutIncreasedSizeStyles }, submenuIconProps: { styles: menuSubIconStyleSet }, text: localeStrings.strings.call.moreButtonGalleryControlLabel, disabled: props.disableButtonsForHoldScreen, subMenuProps: { items: [ { key: 'dynamicSelectionKey', text: localeStrings.strings.call.moreButtonGalleryFloatingLocalLayoutLabel, canCheck: true, itemProps: { styles: buttonFlyoutIncreasedSizeStyles }, isChecked: props.userSetGalleryLayout === 'floatingLocalVideo', onClick: () => { props.onUserSetGalleryLayout && props.onUserSetGalleryLayout('floatingLocalVideo'); setFocusedContentOn(false); }, iconProps: { iconName: 'FloatingLocalVideoGalleryLayout', styles: { root: { lineHeight: 0 } } } }, { key: 'speakerSelectionKey', text: localeStrings.strings.call.moreButtonGallerySpeakerLayoutLabel, canCheck: true, itemProps: { styles: buttonFlyoutIncreasedSizeStyles }, isChecked: props.userSetGalleryLayout === 'speaker', onClick: () => { props.onUserSetGalleryLayout && props.onUserSetGalleryLayout('speaker'); setFocusedContentOn(false); }, iconProps: { iconName: 'SpeakerGalleryLayout', styles: { root: { lineHeight: 0 } } } }, { key: 'focusedContentSelectionKey', text: localeStrings.strings.call.moreButtonGalleryFocusedContentLayoutLabel, canCheck: true, itemProps: { styles: buttonFlyoutIncreasedSizeStyles }, isChecked: focusedContentOn, onClick: () => { if (focusedContentOn === false) { setPreviousLayout(props.userSetGalleryLayout ?? 'floatingLocalVideo'); props.onUserSetGalleryLayout && props.onUserSetGalleryLayout('focusedContent'); setFocusedContentOn(true); } else { props.onUserSetGalleryLayout && props.onUserSetGalleryLayout(previousLayout); setFocusedContentOn(false); } }, iconProps: { iconName: 'FocusedContentGalleryLayout', styles: { root: { lineHeight: 0 } } } } ], calloutProps: { preventDismissOnEvent: _preventDismissOnEvent } } }; /* @conditional-compile-remove(gallery-layout-composite) */ const galleryOption = { key: 'defaultSelectionKey', text: localeStrings.strings.call.moreButtonGalleryDefaultLayoutLabel, canCheck: true, itemProps: { styles: buttonFlyoutIncreasedSizeStyles }, isChecked: props.userSetGalleryLayout === 'default', onClick: () => { props.onUserSetGalleryLayout && props.onUserSetGalleryLayout('default'); setFocusedContentOn(false); }, iconProps: { iconName: 'DefaultGalleryLayout', styles: { root: { lineHeight: 0 } } } }; /* @conditional-compile-remove(large-gallery) */ const largeGalleryOption = { key: 'largeGallerySelectionKey', text: localeStrings.strings.call.moreButtonLargeGalleryDefaultLayoutLabel, canCheck: true, itemProps: { styles: buttonFlyoutIncreasedSizeStyles }, isChecked: props.userSetGalleryLayout === 'largeGallery', onClick: () => { props.onUserSetGalleryLayout && props.onUserSetGalleryLayout('largeGallery'); setFocusedContentOn(false); }, iconProps: { iconName: 'LargeGalleryLayout', styles: { root: { lineHeight: 0 } } } }; /* @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', text: localeStrings.strings.call.moreButtonGalleryPositionToggleLabel, canCheck: true, topDivider: true, itemProps: { styles: buttonFlyoutIncreasedSizeStyles }, iconProps: { iconName: 'OverflowGalleryTop', styles: { root: { lineHeight: 0 } } }, isChecked: galleryPositionTop, onClick: () => { if (galleryPositionTop === false) { props.onUserSetOverflowGalleryPositionChange && props.onUserSetOverflowGalleryPositionChange('horizontalTop'); setGalleryPositionTop(true); } else { props.onUserSetOverflowGalleryPositionChange && props.onUserSetOverflowGalleryPositionChange('Responsive'); setGalleryPositionTop(false); } } }; /* @conditional-compile-remove(large-gallery) */ galleryOptions.subMenuProps?.items?.push(largeGalleryOption); /* @conditional-compile-remove(gallery-layout-composite) */ galleryOptions.subMenuProps?.items?.push(galleryOption); /* @conditional-compile-remove(overflow-top-composite) */ galleryOptions.subMenuProps?.items?.push(overflowGalleryOption); /* @conditional-compile-remove(together-mode) */ if (isTeamsCall || isTeamsMeeting) { galleryOptions.subMenuProps?.items?.push(togetherModeOption); } if (props.callControls === true || (props.callControls as CallControlOptions)?.galleryControlsButton !== false) { moreButtonContextualMenuItems.push(galleryOptions); } } const customDrawerButtons = useMemo( () => generateCustomCallDesktopOverflowButtons( onFetchCustomButtonPropsTrampoline(typeof props.callControls === 'object' ? props.callControls : undefined), typeof props.callControls === 'object' ? props.callControls.displayType : undefined ), [props.callControls] ); customDrawerButtons['primary'].slice(CUSTOM_BUTTON_OPTIONS.MAX_PRIMARY_DESKTOP_CUSTOM_BUTTONS).forEach((element) => { moreButtonContextualMenuItems.push({ itemProps: { styles: buttonFlyoutIncreasedSizeStyles }, ...element }); }); customDrawerButtons['secondary'] .slice(CUSTOM_BUTTON_OPTIONS.MAX_SECONDARY_DESKTOP_CUSTOM_BUTTONS) .forEach((element) => { moreButtonContextualMenuItems.push({ itemProps: { styles: buttonFlyoutIncreasedSizeStyles }, ...element }); }); customDrawerButtons['overflow'].forEach((element) => { moreButtonContextualMenuItems.push({ itemProps: { styles: buttonFlyoutIncreasedSizeStyles }, ...element }); }); return ( <MoreButton {...props} data-ui-id="common-call-composite-more-button" strings={moreButtonStrings} menuIconProps={{ hidden: true }} menuProps={{ shouldFocusOnContainer: false, items: moreButtonContextualMenuItems, calloutProps: { preventDismissOnEvent: _preventDismissOnEvent } }} /> ); };