init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
import { IReduxState } from '../../app/types';
import { AUDIO_MUTE_BUTTON_ENABLED } from '../../base/flags/constants';
import { getFeatureFlag } from '../../base/flags/functions';
import { MEDIA_TYPE } from '../../base/media/constants';
import { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
import BaseAudioMuteButton from '../../base/toolbox/components/BaseAudioMuteButton';
import { isLocalTrackMuted } from '../../base/tracks/functions';
import { muteLocal } from '../../video-menu/actions';
import { isAudioMuteButtonDisabled } from '../functions';
/**
* The type of the React {@code Component} props of {@link AbstractAudioMuteButton}.
*/
export interface IProps extends AbstractButtonProps {
/**
* Whether audio is currently muted or not.
*/
_audioMuted: boolean;
/**
* Whether the button is disabled.
*/
_disabled: boolean;
}
/**
* Component that renders a toolbar button for toggling audio mute.
*
* @augments BaseAudioMuteButton
*/
export default class AbstractAudioMuteButton<P extends IProps> extends BaseAudioMuteButton<P> {
override accessibilityLabel = 'toolbar.accessibilityLabel.mute';
override toggledAccessibilityLabel = 'toolbar.accessibilityLabel.unmute';
override label = 'toolbar.mute';
override toggledLabel = 'toolbar.unmute';
override tooltip = 'toolbar.mute';
override toggledTooltip = 'toolbar.unmute';
/**
* Indicates if audio is currently muted or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isAudioMuted() {
return this.props._audioMuted;
}
/**
* Changes the muted state.
*
* @param {boolean} audioMuted - Whether audio should be muted or not.
* @protected
* @returns {void}
*/
override _setAudioMuted(audioMuted: boolean) {
this.props.dispatch(muteLocal(audioMuted, MEDIA_TYPE.AUDIO));
}
/**
* Return a boolean value indicating if this button is disabled or not.
*
* @returns {boolean}
*/
override _isDisabled() {
return this.props._disabled;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code AbstractAudioMuteButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioMuted: boolean,
* _disabled: boolean
* }}
*/
export function mapStateToProps(state: IReduxState) {
const _audioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
const _disabled = isAudioMuteButtonDisabled(state);
const enabledFlag = getFeatureFlag(state, AUDIO_MUTE_BUTTON_ENABLED, true);
return {
_audioMuted,
_disabled,
visible: enabledFlag
};
}

View File

@@ -0,0 +1,94 @@
import { IReduxState } from '../../app/types';
import { VIDEO_MUTE_BUTTON_ENABLED } from '../../base/flags/constants';
import { getFeatureFlag } from '../../base/flags/functions';
import { MEDIA_TYPE } from '../../base/media/constants';
import { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
import BaseVideoMuteButton from '../../base/toolbox/components/BaseVideoMuteButton';
import { isLocalTrackMuted } from '../../base/tracks/functions';
import { handleToggleVideoMuted } from '../actions.any';
import { isVideoMuteButtonDisabled } from '../functions';
/**
* The type of the React {@code Component} props of {@link AbstractVideoMuteButton}.
*/
export interface IProps extends AbstractButtonProps {
/**
* Whether video button is disabled or not.
*/
_videoDisabled: boolean;
/**
* Whether video is currently muted or not.
*/
_videoMuted: boolean;
}
/**
* Component that renders a toolbar button for toggling video mute.
*
* @augments BaseVideoMuteButton
*/
export default class AbstractVideoMuteButton<P extends IProps> extends BaseVideoMuteButton<P> {
override accessibilityLabel = 'toolbar.accessibilityLabel.videomute';
override toggledAccessibilityLabel = 'toolbar.accessibilityLabel.videounmute';
override label = 'toolbar.videomute';
override toggledLabel = 'toolbar.videounmute';
override tooltip = 'toolbar.videomute';
override toggledTooltip = 'toolbar.videounmute';
/**
* Indicates if video is currently disabled or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isDisabled() {
return this.props._videoDisabled;
}
/**
* Indicates if video is currently muted or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isVideoMuted() {
return this.props._videoMuted;
}
/**
* Changes the muted state.
*
* @override
* @param {boolean} videoMuted - Whether video should be muted or not.
* @protected
* @returns {void}
*/
override _setVideoMuted(videoMuted: boolean) {
this.props.dispatch(handleToggleVideoMuted(videoMuted, true, true));
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code VideoMuteButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _videoMuted: boolean
* }}
*/
export function mapStateToProps(state: IReduxState) {
const tracks = state['features/base/tracks'];
const enabledFlag = getFeatureFlag(state, VIDEO_MUTE_BUTTON_ENABLED, true);
return {
_videoDisabled: isVideoMuteButtonDisabled(state),
_videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO),
visible: enabledFlag
};
}

View File

@@ -0,0 +1,59 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState } from '../../app/types';
import { translate } from '../../base/i18n/functions';
import { IconDownload } from '../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
import { openURLInBrowser } from '../../base/util/openURLInBrowser';
interface IProps extends AbstractButtonProps {
/**
* The URL to the applications page.
*/
_downloadAppsUrl: string;
}
/**
* Implements an {@link AbstractButton} to open the applications page in a new window.
*/
class DownloadButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.download';
override icon = IconDownload;
override label = 'toolbar.download';
override tooltip = 'toolbar.download';
/**
* Handles clicking / pressing the button, and opens a new window with the user documentation.
*
* @private
* @returns {void}
*/
override _handleClick() {
const { _downloadAppsUrl } = this.props;
sendAnalytics(createToolbarEvent('download.pressed'));
openURLInBrowser(_downloadAppsUrl);
}
}
/**
* Maps part of the redux state to the component's props.
*
* @param {Object} state - The redux store/state.
* @returns {Object}
*/
function _mapStateToProps(state: IReduxState) {
const { downloadAppsUrl } = state['features/base/config'].deploymentUrls || {};
const visible = typeof downloadAppsUrl === 'string';
return {
_downloadAppsUrl: downloadAppsUrl ?? '',
visible
};
}
export default translate(connect(_mapStateToProps)(DownloadButton));

View File

@@ -0,0 +1,50 @@
import { once } from 'lodash-es';
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { leaveConference } from '../../base/conference/actions';
import { translate } from '../../base/i18n/functions';
import { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
import AbstractHangupButton from '../../base/toolbox/components/AbstractHangupButton';
/**
* Component that renders a toolbar button for leaving the current conference.
*
* @augments AbstractHangupButton
*/
class HangupButton extends AbstractHangupButton<AbstractButtonProps> {
_hangup: Function;
override accessibilityLabel = 'toolbar.accessibilityLabel.hangup';
override label = 'toolbar.hangup';
override tooltip = 'toolbar.hangup';
/**
* Initializes a new HangupButton instance.
*
* @param {Props} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: AbstractButtonProps) {
super(props);
this._hangup = once(() => {
sendAnalytics(createToolbarEvent('hangup'));
this.props.dispatch(leaveConference());
});
}
/**
* Helper function to perform the actual hangup action.
*
* @override
* @protected
* @returns {void}
*/
override _doHangup() {
this._hangup();
}
}
export default translate(connect()(HangupButton));

View File

@@ -0,0 +1,62 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState } from '../../app/types';
import { HELP_BUTTON_ENABLED } from '../../base/flags/constants';
import { getFeatureFlag } from '../../base/flags/functions';
import { translate } from '../../base/i18n/functions';
import { IconHelp } from '../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
import { openURLInBrowser } from '../../base/util/openURLInBrowser';
interface IProps extends AbstractButtonProps {
/**
* The URL to the user documentation.
*/
_userDocumentationURL: string;
}
/**
* Implements an {@link AbstractButton} to open the user documentation in a new window.
*/
class HelpButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.help';
override icon = IconHelp;
override label = 'toolbar.help';
override tooltip = 'toolbar.help';
/**
* Handles clicking / pressing the button, and opens a new window with the user documentation.
*
* @private
* @returns {void}
*/
override _handleClick() {
const { _userDocumentationURL } = this.props;
sendAnalytics(createToolbarEvent('help.pressed'));
openURLInBrowser(_userDocumentationURL);
}
}
/**
* Maps part of the redux state to the component's props.
*
* @param {Object} state - The redux store/state.
* @returns {Object}
*/
function _mapStateToProps(state: IReduxState) {
const { userDocumentationURL } = state['features/base/config'].deploymentUrls || {};
const enabled = getFeatureFlag(state, HELP_BUTTON_ENABLED, true);
const visible = typeof userDocumentationURL === 'string' && enabled;
return {
_userDocumentationURL: userDocumentationURL ?? '',
visible
};
}
export default translate(connect(_mapStateToProps)(HelpButton));

View File

@@ -0,0 +1 @@
export { default as CustomOptionButton } from './native/CustomOptionButton';

View File

@@ -0,0 +1 @@
export { default as CustomOptionButton } from './web/CustomOptionButton';

View File

@@ -0,0 +1,6 @@
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import AbstractAudioMuteButton, { IProps, mapStateToProps } from '../AbstractAudioMuteButton';
export default translate(connect(mapStateToProps)(AbstractAudioMuteButton<IProps>));

View File

@@ -0,0 +1,96 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { setAudioOnly, toggleAudioOnly } from '../../../base/audio-only/actions';
import { AUDIO_ONLY_BUTTON_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconAudioOnly, IconAudioOnlyOff } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import {
navigate
} from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
/**
* The type of the React {@code Component} props of {@link AudioOnlyButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether the current conference is in audio only mode or not.
*/
_audioOnly: boolean;
/**
* Indicates whether the car mode is enabled.
*/
_startCarMode?: boolean;
}
/**
* An implementation of a button for toggling the audio-only mode.
*/
class AudioOnlyButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.audioOnly';
override icon = IconAudioOnly;
override label = 'toolbar.audioOnlyOn';
override toggledIcon = IconAudioOnlyOff;
override toggledLabel = 'toolbar.audioOnlyOff';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
const { _audioOnly, _startCarMode, dispatch } = this.props;
if (!_audioOnly && _startCarMode) {
dispatch(setAudioOnly(true));
navigate(screen.conference.carmode);
} else {
dispatch(toggleAudioOnly());
}
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._audioOnly;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code AudioOnlyButton} component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The properties explicitly passed to the component instance.
* @private
* @returns {{
* _audioOnly: boolean
* }}
*/
function _mapStateToProps(state: IReduxState, ownProps: any) {
const { enabled: audioOnly } = state['features/base/audio-only'];
const enabledInFeatureFlags = getFeatureFlag(state, AUDIO_ONLY_BUTTON_ENABLED, true);
const { startCarMode } = state['features/base/settings'];
const { visible = enabledInFeatureFlags } = ownProps;
return {
_audioOnly: Boolean(audioOnly),
_startCarMode: startCarMode,
visible
};
}
export default translate(connect(_mapStateToProps)(AudioOnlyButton));

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { Image, View, ViewStyle } from 'react-native';
import { SvgCssUri } from 'react-native-svg/css';
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import styles from './styles';
export interface ICustomOptionButton extends AbstractButtonProps {
backgroundColor?: string;
icon: any;
id?: string;
isToolboxButton?: boolean;
text: string;
}
/**
* Component that renders a custom button.
*
* @returns {Component}
*/
class CustomOptionButton extends AbstractButton<ICustomOptionButton> {
backgroundColor = this.props.backgroundColor;
iconSrc = this.props.icon;
id = this.props.id;
text = this.props.text;
/**
* Custom icon component.
*
* @returns {React.Component}
*/
icon = () => {
let iconComponent;
if (!this.iconSrc) {
return null;
}
if (this.iconSrc?.includes('svg')) {
iconComponent = (
<SvgCssUri
// @ts-ignore
height = { BaseTheme.spacing[4] }
uri = { this.iconSrc }
width = { BaseTheme.spacing[4] } />
);
} else {
iconComponent = (
<Image
height = { BaseTheme.spacing[4] }
resizeMode = { 'contain' }
source = {{ uri: this.iconSrc }}
width = { BaseTheme.spacing[4] } />
);
}
return (
<View
style = { this.props.isToolboxButton && [
styles.toolboxButtonIconContainer,
{ backgroundColor: this.backgroundColor } ] as ViewStyle[] }>
{ iconComponent }
</View>
);
};
label = this.text || '';
}
export default translate(connect()(CustomOptionButton));

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import HangupButton from '../HangupButton';
import HangupMenuButton from './HangupMenuButton';
const HangupContainerButtons = (props: AbstractButtonProps) => {
const { conference } = useSelector((state: IReduxState) => state['features/base/conference']);
const endConferenceSupported = conference?.isEndConferenceSupported();
return endConferenceSupported
// @ts-ignore
? <HangupMenuButton { ...props } />
: <HangupButton { ...props } />;
};
export default HangupContainerButtons;

View File

@@ -0,0 +1,77 @@
import React, { useCallback } from 'react';
import { View } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { createBreakoutRoomsEvent, createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { appNavigate } from '../../../app/actions';
import { IReduxState } from '../../../app/types';
import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry';
import { endConference } from '../../../base/conference/actions';
import { hideSheet } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import { PARTICIPANT_ROLE } from '../../../base/participants/constants';
import { getLocalParticipant } from '../../../base/participants/functions';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { moveToRoom } from '../../../breakout-rooms/actions';
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
/**
* Menu presenting options to leave a room or meeting and to end meeting.
*
* @returns {JSX.Element} - The hangup menu.
*/
function HangupMenu() {
const dispatch = useDispatch();
const _styles: any = useSelector((state: IReduxState) => ColorSchemeRegistry.get(state, 'Toolbox'));
const inBreakoutRoom = useSelector(isInBreakoutRoom);
const isModerator = useSelector((state: IReduxState) =>
getLocalParticipant(state)?.role === PARTICIPANT_ROLE.MODERATOR);
const { DESTRUCTIVE, SECONDARY } = BUTTON_TYPES;
const handleEndConference = useCallback(() => {
dispatch(hideSheet());
sendAnalytics(createToolbarEvent('endmeeting'));
dispatch(endConference());
}, [ hideSheet ]);
const handleLeaveConference = useCallback(() => {
dispatch(hideSheet());
sendAnalytics(createToolbarEvent('hangup'));
dispatch(appNavigate(undefined));
}, [ hideSheet ]);
const handleLeaveBreakoutRoom = useCallback(() => {
dispatch(hideSheet());
sendAnalytics(createBreakoutRoomsEvent('leave'));
dispatch(moveToRoom());
}, [ hideSheet ]);
return (
<BottomSheet>
<View style = { _styles.hangupMenuContainer }>
{ isModerator && <Button
accessibilityLabel = 'toolbar.endConference'
labelKey = 'toolbar.endConference'
onClick = { handleEndConference }
style = { _styles.hangupButton }
type = { DESTRUCTIVE } /> }
<Button
accessibilityLabel = 'toolbar.leaveConference'
labelKey = 'toolbar.leaveConference'
onClick = { handleLeaveConference }
style = { _styles.hangupButton }
type = { SECONDARY } />
{ inBreakoutRoom && <Button
accessibilityLabel = 'breakoutRooms.actions.leaveBreakoutRoom'
labelKey = 'breakoutRooms.actions.leaveBreakoutRoom'
onClick = { handleLeaveBreakoutRoom }
style = { _styles.hangupButton }
type = { SECONDARY } /> }
</View>
</BottomSheet>
);
}
export default HangupMenu;

View File

@@ -0,0 +1,32 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { openSheet } from '../../../base/dialog/actions';
import { IconHangup } from '../../../base/icons/svg';
import IconButton from '../../../base/ui/components/native/IconButton';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import HangupMenu from './HangupMenu';
/**
* Button for showing the hangup menu.
*
* @returns {JSX.Element} - The hangup menu button.
*/
const HangupMenuButton = (): JSX.Element => {
const dispatch = useDispatch();
const onSelect = useCallback(() => {
dispatch(openSheet(HangupMenu));
}, [ dispatch ]);
return (
<IconButton
accessibilityLabel = 'toolbar.accessibilityLabel.hangup'
onPress = { onSelect }
src = { IconHangup }
type = { BUTTON_TYPES.PRIMARY } />
);
};
export default HangupMenuButton;

View File

@@ -0,0 +1,48 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconCloudUpload } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { navigate }
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import { isSalesforceEnabled } from '../../../salesforce/functions';
/**
* Implementation of a button for opening the Salesforce link dialog.
*/
class LinkToSalesforceButton extends AbstractButton<AbstractButtonProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.linkToSalesforce';
override icon = IconCloudUpload;
override label = 'toolbar.linkToSalesforce';
/**
* Handles clicking / pressing the button, and opens the Salesforce link dialog.
*
* @protected
* @returns {void}
*/
override _handleClick() {
sendAnalytics(createToolbarEvent('link.to.salesforce'));
return navigate(screen.conference.salesforce);
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @private
* @returns {Props}
*/
function mapStateToProps(state: IReduxState) {
return {
visible: isSalesforceEnabled(state)
};
}
export default translate(connect(mapStateToProps)(LinkToSalesforceButton));

View File

@@ -0,0 +1,49 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { CAR_MODE_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconCar } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { navigate }
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
/**
* Implements an {@link AbstractButton} to open the carmode.
*/
class OpenCarmodeButton extends AbstractButton<AbstractButtonProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.carmode';
override icon = IconCar;
override label = 'carmode.labels.buttonLabel';
/**
* Handles clicking / pressing the button, and opens the carmode mode.
*
* @private
* @returns {void}
*/
override _handleClick() {
return navigate(screen.conference.carmode);
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {IReduxState} state - The Redux state.
* @param {AbstractButtonProps} ownProps - The properties explicitly passed to the component instance.
* @private
* @returns {Object}
*/
function _mapStateToProps(state: IReduxState, ownProps: AbstractButtonProps): Object {
const enabled = getFeatureFlag(state, CAR_MODE_ENABLED, true);
const { visible = enabled } = ownProps;
return {
visible
};
}
export default translate(connect(_mapStateToProps)(OpenCarmodeButton));

View File

@@ -0,0 +1,316 @@
import React, { PureComponent } from 'react';
import { ViewStyle } from 'react-native';
import { Divider } from 'react-native-paper';
import { connect, useSelector } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import { hideSheet } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import { bottomSheetStyles } from '../../../base/dialog/components/native/styles';
import SettingsButton from '../../../base/settings/components/native/SettingsButton';
import BreakoutRoomsButton
from '../../../breakout-rooms/components/native/BreakoutRoomsButton';
import SharedDocumentButton from '../../../etherpad/components/SharedDocumentButton.native';
import ReactionMenu from '../../../reactions/components/native/ReactionMenu';
import { shouldDisplayReactionsButtons } from '../../../reactions/functions.any';
import LiveStreamButton from '../../../recording/components/LiveStream/native/LiveStreamButton';
import RecordButton from '../../../recording/components/Recording/native/RecordButton';
import SecurityDialogButton
from '../../../security/components/security-dialog/native/SecurityDialogButton';
import SharedVideoButton from '../../../shared-video/components/native/SharedVideoButton';
import { isSharedVideoEnabled } from '../../../shared-video/functions';
import SpeakerStatsButton from '../../../speaker-stats/components/native/SpeakerStatsButton';
import { isSpeakerStatsDisabled } from '../../../speaker-stats/functions';
import ClosedCaptionButton from '../../../subtitles/components/native/ClosedCaptionButton';
import styles from '../../../video-menu/components/native/styles';
import { iAmVisitor } from '../../../visitors/functions';
import WhiteboardButton from '../../../whiteboard/components/native/WhiteboardButton';
import { customButtonPressed } from '../../actions.native';
import { getVisibleNativeButtons } from '../../functions.native';
import { useNativeToolboxButtons } from '../../hooks.native';
import { IToolboxNativeButton } from '../../types';
import AudioOnlyButton from './AudioOnlyButton';
import LinkToSalesforceButton from './LinkToSalesforceButton';
import OpenCarmodeButton from './OpenCarmodeButton';
import RaiseHandButton from './RaiseHandButton';
/**
* The type of the React {@code Component} props of {@link OverflowMenu}.
*/
interface IProps {
/**
* True if breakout rooms feature is available, false otherwise.
*/
_isBreakoutRoomsSupported?: boolean;
/**
* True if the overflow menu is currently visible, false otherwise.
*/
_isOpen: boolean;
/**
* Whether the shared video is enabled or not.
*/
_isSharedVideoEnabled: boolean;
/**
* Whether or not speaker stats is disable.
*/
_isSpeakerStatsDisabled?: boolean;
/**
* Toolbar buttons.
*/
_mainMenuButtons?: Array<IToolboxNativeButton>;
/**
* Overflow menu buttons.
*/
_overflowMenuButtons?: Array<IToolboxNativeButton>;
/**
* Whether the recoding button should be enabled or not.
*/
_recordingEnabled: boolean;
/**
* Whether or not any reactions buttons should be displayed.
*/
_shouldDisplayReactionsButtons: boolean;
/**
* Used for hiding the dialog when the selection was completed.
*/
dispatch: IStore['dispatch'];
}
interface IState {
/**
* True if the bottom sheet is scrolled to the top.
*/
scrolledToTop: boolean;
}
/**
* Implements a React {@code Component} with some extra actions in addition to
* those in the toolbar.
*/
class OverflowMenu extends PureComponent<IProps, IState> {
/**
* Initializes a new {@code OverflowMenu} instance.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this.state = {
scrolledToTop: true
};
// Bind event handlers so they are only bound once per instance.
this._onCancel = this._onCancel.bind(this);
this._renderReactionMenu = this._renderReactionMenu.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
const {
_isBreakoutRoomsSupported,
_isSpeakerStatsDisabled,
_isSharedVideoEnabled,
dispatch
} = this.props;
const buttonProps = {
afterClick: this._onCancel,
showLabel: true,
styles: bottomSheetStyles.buttons
};
const topButtonProps = {
afterClick: this._onCancel,
dispatch,
showLabel: true,
styles: {
...bottomSheetStyles.buttons,
style: {
...bottomSheetStyles.buttons.style,
borderTopLeftRadius: 16,
borderTopRightRadius: 16
}
}
};
return (
<BottomSheet
renderFooter = { this._renderReactionMenu }>
<Divider style = { styles.divider as ViewStyle } />
<OpenCarmodeButton { ...topButtonProps } />
<AudioOnlyButton { ...buttonProps } />
{ this._renderRaiseHandButton(buttonProps) }
{/* @ts-ignore */}
<SecurityDialogButton { ...buttonProps } />
<RecordButton { ...buttonProps } />
<LiveStreamButton { ...buttonProps } />
<LinkToSalesforceButton { ...buttonProps } />
<WhiteboardButton { ...buttonProps } />
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
{_isSharedVideoEnabled && <SharedVideoButton { ...buttonProps } />}
{ this._renderOverflowMenuButtons(topButtonProps) }
{!_isSpeakerStatsDisabled && <SpeakerStatsButton { ...buttonProps } />}
{_isBreakoutRoomsSupported && <BreakoutRoomsButton { ...buttonProps } />}
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
<ClosedCaptionButton { ...buttonProps } />
<SharedDocumentButton { ...buttonProps } />
<SettingsButton { ...buttonProps } />
</BottomSheet>
);
}
/**
* Hides this {@code OverflowMenu}.
*
* @private
* @returns {void}
*/
_onCancel() {
this.props.dispatch(hideSheet());
}
/**
* Function to render the reaction menu as the footer of the bottom sheet.
*
* @returns {React.ReactElement}
*/
_renderReactionMenu() {
const { _mainMenuButtons, _shouldDisplayReactionsButtons } = this.props;
// @ts-ignore
const isRaiseHandInMainMenu = _mainMenuButtons?.some(item => item.key === 'raisehand');
if (_shouldDisplayReactionsButtons && !isRaiseHandInMainMenu) {
return (
<ReactionMenu
onCancel = { this._onCancel }
overflowMenu = { true } />
);
}
}
/**
* Function to render the reaction menu as the footer of the bottom sheet.
*
* @param {Object} buttonProps - Styling button properties.
* @returns {React.ReactElement}
*/
_renderRaiseHandButton(buttonProps: Object) {
const { _mainMenuButtons, _shouldDisplayReactionsButtons } = this.props;
// @ts-ignore
const isRaiseHandInMainMenu = _mainMenuButtons?.some(item => item.key === 'raisehand');
if (!_shouldDisplayReactionsButtons && !isRaiseHandInMainMenu) {
return (
<RaiseHandButton { ...buttonProps } />
);
}
}
/**
* Function to render the custom buttons for the overflow menu.
*
* @param {Object} topButtonProps - Styling button properties.
* @returns {React.ReactElement}
*/
_renderOverflowMenuButtons(topButtonProps: Object) {
const { _overflowMenuButtons, dispatch } = this.props;
if (!_overflowMenuButtons?.length) {
return;
}
return (
<>
{
_overflowMenuButtons?.map(({ Content, key, text, ...rest }: IToolboxNativeButton) => {
if (key === 'raisehand') {
return null;
}
return (
<Content
{ ...topButtonProps }
{ ...rest }
/* eslint-disable react/jsx-no-bind */
handleClick = { () => dispatch(customButtonPressed(key, text)) }
isToolboxButton = { false }
key = { key }
text = { text } />
);
})
}
<Divider style = { styles.divider as ViewStyle } />
</>
);
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
const { conference } = state['features/base/conference'];
return {
_isBreakoutRoomsSupported: conference?.getBreakoutRooms()?.isSupported(),
_isSharedVideoEnabled: isSharedVideoEnabled(state),
_isSpeakerStatsDisabled: isSpeakerStatsDisabled(state),
_shouldDisplayReactionsButtons: shouldDisplayReactionsButtons(state)
};
}
export default connect(_mapStateToProps)(props => {
const { clientWidth } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
const { customToolbarButtons } = useSelector((state: IReduxState) => state['features/base/config']);
const {
mainToolbarButtonsThresholds,
toolbarButtons
} = useSelector((state: IReduxState) => state['features/toolbox']);
const _iAmVisitor = useSelector(iAmVisitor);
const allButtons = useNativeToolboxButtons(customToolbarButtons);
const { mainMenuButtons, overflowMenuButtons } = getVisibleNativeButtons({
allButtons,
clientWidth,
mainToolbarButtonsThresholds,
toolbarButtons,
iAmVisitor: _iAmVisitor
});
return (
<OverflowMenu
// @ts-ignore
{ ... props }
_mainMenuButtons = { mainMenuButtons }
_overflowMenuButtons = { overflowMenuButtons } />
);
});

View File

@@ -0,0 +1,50 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { openSheet } from '../../../base/dialog/actions';
import { OVERFLOW_MENU_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconDotsHorizontal } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import OverflowMenu from './OverflowMenu';
/**
* An implementation of a button for showing the {@code OverflowMenu}.
*/
class OverflowMenuButton extends AbstractButton<AbstractButtonProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.moreActions';
override icon = IconDotsHorizontal;
override label = 'toolbar.moreActions';
/**
* Handles clicking / pressing this {@code OverflowMenuButton}.
*
* @protected
* @returns {void}
*/
override _handleClick() {
// @ts-ignore
this.props.dispatch(openSheet(OverflowMenu));
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code OverflowMenuButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function _mapStateToProps(state: IReduxState) {
const enabledFlag = getFeatureFlag(state, OVERFLOW_MENU_ENABLED, true);
return {
visible: enabledFlag
};
}
export default translate(connect(_mapStateToProps)(OverflowMenuButton));

View File

@@ -0,0 +1,99 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { RAISE_HAND_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconRaiseHand } from '../../../base/icons/svg';
import { raiseHand } from '../../../base/participants/actions';
import {
getLocalParticipant,
hasRaisedHand
} from '../../../base/participants/functions';
import { ILocalParticipant } from '../../../base/participants/types';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
/**
* The type of the React {@code Component} props of {@link RaiseHandButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* The local participant.
*/
_localParticipant?: ILocalParticipant;
/**
* Whether the participant raised their hand or not.
*/
_raisedHand: boolean;
}
/**
* An implementation of a button to raise or lower hand.
*/
class RaiseHandButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
override icon = IconRaiseHand;
override label = 'toolbar.raiseYourHand';
override toggledLabel = 'toolbar.lowerYourHand';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
this._toggleRaisedHand();
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._raisedHand;
}
/**
* Toggles the rased hand status of the local participant.
*
* @returns {void}
*/
_toggleRaisedHand() {
const enable = !this.props._raisedHand;
sendAnalytics(createToolbarEvent('raise.hand', { enable }));
this.props.dispatch(raiseHand(enable));
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The properties explicitly passed to the component instance.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState, ownProps: any) {
const _localParticipant = getLocalParticipant(state);
const enabled = getFeatureFlag(state, RAISE_HAND_ENABLED, true);
const { visible = enabled } = ownProps;
return {
_localParticipant,
_raisedHand: hasRaisedHand(_localParticipant),
visible
};
}
export default translate(connect(_mapStateToProps)(RaiseHandButton));

View File

@@ -0,0 +1,91 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { ANDROID_SCREENSHARING_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconScreenshare } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { toggleScreensharing } from '../../../base/tracks/actions.native';
import { isLocalVideoTrackDesktop } from '../../../base/tracks/functions.native';
/**
* The type of the React {@code Component} props of {@link ScreenSharingAndroidButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* True if the button needs to be disabled.
*/
_disabled: boolean;
/**
* Whether video is currently muted or not.
*/
_screensharing: boolean;
}
/**
* An implementation of a button for toggling screen sharing.
*/
class ScreenSharingAndroidButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.shareYourScreen';
override icon = IconScreenshare;
override label = 'toolbar.startScreenSharing';
override toggledLabel = 'toolbar.stopScreenSharing';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
const enable = !this._isToggled();
this.props.dispatch(toggleScreensharing(enable));
}
/**
* Returns a boolean value indicating if this button is disabled or not.
*
* @protected
* @returns {boolean}
*/
_isDisabled() {
return this.props._disabled;
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._screensharing;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ToggleCameraButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _screensharing: boolean
* }}
*/
function _mapStateToProps(state: IReduxState) {
const enabled = getFeatureFlag(state, ANDROID_SCREENSHARING_ENABLED, true);
return {
_screensharing: isLocalVideoTrackDesktop(state),
visible: enabled
};
}
export default translate(connect(_mapStateToProps)(ScreenSharingAndroidButton));

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { Platform } from 'react-native';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { isDesktopShareButtonDisabled } from '../../functions.native';
import ScreenSharingAndroidButton from './ScreenSharingAndroidButton';
import ScreenSharingIosButton from './ScreenSharingIosButton';
const ScreenSharingButton = (props: any) => (
<>
{Platform.OS === 'android'
&& <ScreenSharingAndroidButton { ...props } />
}
{Platform.OS === 'ios'
&& <ScreenSharingIosButton { ...props } />
}
</>
);
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ScreenSharingButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Object}
*/
function _mapStateToProps(state: IReduxState) {
return {
_disabled: isDesktopShareButtonDisabled(state)
};
}
export default connect(_mapStateToProps)(ScreenSharingButton);

View File

@@ -0,0 +1,148 @@
import React from 'react';
import { NativeModules, Platform, findNodeHandle } from 'react-native';
import { ScreenCapturePickerView } from 'react-native-webrtc';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { IOS_SCREENSHARING_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconScreenshare } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { isLocalVideoTrackDesktop } from '../../../base/tracks/functions.native';
/**
* The type of the React {@code Component} props of {@link ScreenSharingIosButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* True if the button needs to be disabled.
*/
_disabled: boolean;
/**
* Whether video is currently muted or not.
*/
_screensharing: boolean;
}
const styles = {
screenCapturePickerView: {
display: 'none'
}
};
/**
* An implementation of a button for toggling screen sharing on iOS.
*/
class ScreenSharingIosButton extends AbstractButton<IProps> {
_nativeComponent: React.Component<any, any> | null;
override accessibilityLabel = 'toolbar.accessibilityLabel.shareYourScreen';
override icon = IconScreenshare;
override label = 'toolbar.startScreenSharing';
override toggledLabel = 'toolbar.stopScreenSharing';
/**
* Initializes a new {@code ScreenSharingIosButton} instance.
*
* @param {Object} props - The React {@code Component} props to initialize
* the new {@code ScreenSharingIosButton} instance with.
*/
constructor(props: IProps) {
super(props);
this._nativeComponent = null;
// Bind event handlers so they are only bound once per instance.
this._setNativeComponent = this._setNativeComponent.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {React$Node}
*/
override render() {
return (
<>
{ super.render() }
<ScreenCapturePickerView
ref = { this._setNativeComponent } // @ts-ignore
style = { styles.screenCapturePickerView } />
</>
);
}
/**
* Sets the internal reference to the React Component wrapping the
* {@code RPSystemBroadcastPickerView} component.
*
* @param {ReactComponent} component - React Component.
* @returns {void}
*/
_setNativeComponent(component: React.Component<any, any> | null) {
this._nativeComponent = component;
}
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
const handle = findNodeHandle(this._nativeComponent);
NativeModules.ScreenCapturePickerViewManager.show(handle);
}
/**
* Returns a boolean value indicating if this button is disabled or not.
*
* @protected
* @returns {boolean}
*/
_isDisabled() {
return this.props._disabled;
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._screensharing;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ScreenSharingIosButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _disabled: boolean,
* }}
*/
function _mapStateToProps(state: IReduxState) {
const enabled = getFeatureFlag(state, IOS_SCREENSHARING_ENABLED, false);
return {
_screensharing: isLocalVideoTrackDesktop(state),
// TODO: this should work on iOS 12 too, but our trick to show the picker doesn't work.
visible: enabled
&& Platform.OS === 'ios'
&& Number.parseInt(Platform.Version.split('.')[0], 10) >= 14
};
}
export default translate(connect(_mapStateToProps)(ScreenSharingIosButton));

View File

@@ -0,0 +1,79 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconCameraRefresh } from '../../../base/icons/svg';
import { toggleCameraFacingMode } from '../../../base/media/actions';
import { MEDIA_TYPE } from '../../../base/media/constants';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { isLocalTrackMuted } from '../../../base/tracks/functions.native';
/**
* The type of the React {@code Component} props of {@link ToggleCameraButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether the current conference is in audio only mode or not.
*/
_audioOnly: boolean;
/**
* Whether video is currently muted or not.
*/
_videoMuted: boolean;
}
/**
* An implementation of a button for toggling the camera facing mode.
*/
class ToggleCameraButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.toggleCamera';
override icon = IconCameraRefresh;
override label = 'toolbar.toggleCamera';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
this.props.dispatch(toggleCameraFacingMode());
}
/**
* Indicates whether this button is disabled or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isDisabled() {
return this.props._audioOnly || this.props._videoMuted;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ToggleCameraButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioOnly: boolean,
* _videoMuted: boolean
* }}
*/
function _mapStateToProps(state: IReduxState) {
const { enabled: audioOnly } = state['features/base/audio-only'];
const tracks = state['features/base/tracks'];
return {
_audioOnly: Boolean(audioOnly),
_videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO)
};
}
export default translate(connect(_mapStateToProps)(ToggleCameraButton));

View File

@@ -0,0 +1,74 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconAudioOnlyOff } from '../../../base/icons/svg';
import { updateSettings } from '../../../base/settings/actions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
/**
* The type of the React {@code Component} props of {@link ToggleSelfViewButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether the self view is disabled or not.
*/
_disableSelfView: boolean;
}
/**
* An implementation of a button for toggling the self view.
*/
class ToggleSelfViewButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.selfView';
override icon = IconAudioOnlyOff;
override label = 'videothumbnail.hideSelfView';
override toggledLabel = 'videothumbnail.showSelfView';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
const { _disableSelfView, dispatch } = this.props;
dispatch(updateSettings({
disableSelfView: !_disableSelfView
}));
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._disableSelfView;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ToggleSelfViewButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _disableSelfView: boolean
* }}
*/
function _mapStateToProps(state: IReduxState) {
const { disableSelfView } = state['features/base/settings'];
return {
_disableSelfView: Boolean(disableSelfView)
};
}
export default translate(connect(_mapStateToProps)(ToggleSelfViewButton));

View File

@@ -0,0 +1,142 @@
import React from 'react';
import { View, ViewStyle } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { connect, useSelector } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry';
import Platform from '../../../base/react/Platform.native';
import { iAmVisitor } from '../../../visitors/functions';
import { customButtonPressed } from '../../actions.native';
import { getVisibleNativeButtons, isToolboxVisible } from '../../functions.native';
import { useNativeToolboxButtons } from '../../hooks.native';
import { IToolboxNativeButton } from '../../types';
import styles from './styles';
/**
* The type of {@link Toolbox}'s React {@code Component} props.
*/
interface IProps {
/**
* Whether we are in visitors mode.
*/
_iAmVisitor: boolean;
/**
* The color-schemed stylesheet of the feature.
*/
_styles: any;
/**
* The indicator which determines whether the toolbox is visible.
*/
_visible: boolean;
/**
* Redux store dispatch method.
*/
dispatch: IStore['dispatch'];
}
/**
* Implements the conference Toolbox on React Native.
*
* @param {Object} props - The props of the component.
* @returns {React$Element}
*/
function Toolbox(props: IProps) {
const {
_iAmVisitor,
_styles,
_visible,
dispatch
} = props;
if (!_visible) {
return null;
}
const { clientWidth } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
const { customToolbarButtons } = useSelector((state: IReduxState) => state['features/base/config']);
const {
mainToolbarButtonsThresholds,
toolbarButtons
} = useSelector((state: IReduxState) => state['features/toolbox']);
const allButtons = useNativeToolboxButtons(customToolbarButtons);
const { mainMenuButtons } = getVisibleNativeButtons({
allButtons,
clientWidth,
iAmVisitor: _iAmVisitor,
mainToolbarButtonsThresholds,
toolbarButtons
});
const bottomEdge = Platform.OS === 'ios' && _visible;
const { buttonStylesBorderless, hangupButtonStyles } = _styles;
const style = { ...styles.toolbox };
// We have only hangup and raisehand button in _iAmVisitor mode
if (_iAmVisitor) {
style.justifyContent = 'center';
}
const renderToolboxButtons = () => {
if (!mainMenuButtons?.length) {
return;
}
return (
<>
{
mainMenuButtons?.map(({ Content, key, text, ...rest }: IToolboxNativeButton) => (
<Content
{ ...rest }
/* eslint-disable react/jsx-no-bind */
handleClick = { () => dispatch(customButtonPressed(key, text)) }
isToolboxButton = { true }
key = { key }
styles = { key === 'hangup' ? hangupButtonStyles : buttonStylesBorderless } />
))
}
</>
);
};
return (
<View
style = { styles.toolboxContainer as ViewStyle }>
<SafeAreaView
accessibilityRole = 'toolbar'
// @ts-ignore
edges = { [ bottomEdge && 'bottom' ].filter(Boolean) }
pointerEvents = 'box-none'
style = { style as ViewStyle }>
{ renderToolboxButtons() }
</SafeAreaView>
</View>
);
}
/**
* Maps parts of the redux state to {@link Toolbox} (React {@code Component})
* props.
*
* @param {Object} state - The redux state of which parts are to be mapped to
* {@code Toolbox} props.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
return {
_iAmVisitor: iAmVisitor(state),
_styles: ColorSchemeRegistry.get(state, 'Toolbox'),
_visible: isToolboxVisible(state),
};
}
export default connect(_mapStateToProps)(Toolbox);

View File

@@ -0,0 +1,7 @@
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import AbstractVideoMuteButton, { IProps, mapStateToProps } from '../AbstractVideoMuteButton';
export default translate(connect(mapStateToProps)(AbstractVideoMuteButton<IProps>));

View File

@@ -0,0 +1,216 @@
import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry';
import { schemeColor } from '../../../base/color-scheme/functions';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
const BUTTON_SIZE = 48;
// Toolbox, toolbar:
/**
* The style of toolbar buttons.
*/
const toolbarButton = {
borderRadius: BaseTheme.shape.borderRadius,
borderWidth: 0,
flex: 0,
flexDirection: 'row',
height: BUTTON_SIZE,
justifyContent: 'center',
marginHorizontal: 6,
marginVertical: 6,
width: BUTTON_SIZE
};
/**
* The icon style of the toolbar buttons.
*/
const toolbarButtonIcon = {
alignSelf: 'center',
color: BaseTheme.palette.icon04,
fontSize: 24
};
/**
* The icon style of toolbar buttons which display white icons.
*/
const whiteToolbarButtonIcon = {
...toolbarButtonIcon,
color: BaseTheme.palette.icon01
};
/**
* The style of reaction buttons.
*/
const reactionButton = {
...toolbarButton,
backgroundColor: 'transparent',
alignItems: 'center',
marginTop: 0,
marginHorizontal: 0
};
const gifButton = {
...reactionButton,
backgroundColor: '#000'
};
/**
* The style of the emoji on the reaction buttons.
*/
const reactionEmoji = {
fontSize: 20,
color: BaseTheme.palette.icon01
};
const reactionMenu = {
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: BaseTheme.palette.ui01
};
/**
* The Toolbox and toolbar related styles.
*/
const styles = {
sheetGestureRecognizer: {
alignItems: 'stretch',
flexDirection: 'column'
},
/**
* The style of the toolbar.
*/
toolbox: {
alignItems: 'center',
backgroundColor: BaseTheme.palette.uiBackground,
borderTopLeftRadius: 3,
borderTopRightRadius: 3,
flexDirection: 'row',
justifyContent: 'space-between'
},
/**
* The style of the root/top-level container of {@link Toolbox}.
*/
toolboxContainer: {
backgroundColor: BaseTheme.palette.uiBackground,
flexDirection: 'column',
maxWidth: 580,
marginHorizontal: 'auto',
marginVertical: BaseTheme.spacing[0],
paddingHorizontal: BaseTheme.spacing[2],
width: '100%'
},
toolboxButtonIconContainer: {
alignItems: 'center',
borderRadius: BaseTheme.shape.borderRadius,
height: BaseTheme.spacing[7],
justifyContent: 'center',
width: BaseTheme.spacing[7]
}
};
export default styles;
/**
* Color schemed styles for the @{Toolbox} component.
*/
ColorSchemeRegistry.register('Toolbox', {
/**
* Styles for buttons in the toolbar.
*/
buttonStyles: {
iconStyle: toolbarButtonIcon,
style: toolbarButton
},
buttonStylesBorderless: {
iconStyle: whiteToolbarButtonIcon,
style: {
...toolbarButton,
backgroundColor: 'transparent'
},
underlayColor: 'transparent'
},
backgroundToggle: {
backgroundColor: BaseTheme.palette.ui04
},
hangupMenuContainer: {
marginHorizontal: BaseTheme.spacing[2],
marginVertical: BaseTheme.spacing[2]
},
hangupButton: {
flex: 1,
marginHorizontal: BaseTheme.spacing[2],
marginVertical: BaseTheme.spacing[2]
},
hangupButtonStyles: {
iconStyle: whiteToolbarButtonIcon,
style: {
...toolbarButton,
backgroundColor: schemeColor('hangup')
},
underlayColor: BaseTheme.palette.ui04
},
reactionDialog: {
position: 'absolute',
width: '100%',
height: '100%',
backgroundColor: 'transparent'
},
overflowReactionMenu: {
...reactionMenu,
padding: BaseTheme.spacing[3]
},
reactionMenu: {
...reactionMenu,
paddingHorizontal: BaseTheme.spacing[3],
borderRadius: 3,
width: 360
},
reactionRow: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between'
},
reactionButton: {
gifButton,
style: reactionButton,
underlayColor: BaseTheme.palette.ui04,
emoji: reactionEmoji
},
emojiAnimation: {
color: BaseTheme.palette.icon01,
position: 'absolute',
zIndex: 1001,
elevation: 2,
fontSize: 20,
left: '50%',
top: '100%'
},
/**
* Styles for toggled buttons in the toolbar.
*/
toggledButtonStyles: {
iconStyle: whiteToolbarButtonIcon,
style: {
...toolbarButton
},
underlayColor: 'transparent'
}
});

View File

@@ -0,0 +1,205 @@
import React, { ReactElement } from 'react';
import { connect } from 'react-redux';
import { withStyles } from 'tss-react/mui';
import { ACTION_SHORTCUT_TRIGGERED, AUDIO_MUTE, createShortcutEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IGUMPendingState } from '../../../base/media/types';
import AbstractButton from '../../../base/toolbox/components/AbstractButton';
import Spinner from '../../../base/ui/components/web/Spinner';
import { registerShortcut, unregisterShortcut } from '../../../keyboard-shortcuts/actions';
import { SPINNER_COLOR } from '../../constants';
import AbstractAudioMuteButton, {
IProps as AbstractAudioMuteButtonProps,
mapStateToProps as abstractMapStateToProps
} from '../AbstractAudioMuteButton';
const styles = () => {
return {
pendingContainer: {
position: 'absolute' as const,
bottom: '3px',
right: '3px'
}
};
};
/**
* The type of the React {@code Component} props of {@link AudioMuteButton}.
*/
interface IProps extends AbstractAudioMuteButtonProps {
/**
* The gumPending state from redux.
*/
_gumPending: IGUMPendingState;
/**
* An object containing the CSS classes.
*/
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
}
/**
* Component that renders a toolbar button for toggling audio mute.
*
* @augments AbstractAudioMuteButton
*/
class AudioMuteButton extends AbstractAudioMuteButton<IProps> {
/**
* Initializes a new {@code AudioMuteButton} instance.
*
* @param {IProps} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props: IProps) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onKeyboardShortcut = this._onKeyboardShortcut.bind(this);
this._getTooltip = this._getLabel;
}
/**
* Registers the keyboard shortcut that toggles the audio muting.
*
* @inheritdoc
* @returns {void}
*/
override componentDidMount() {
this.props.dispatch(registerShortcut({
character: 'M',
helpDescription: 'keyboardShortcuts.mute',
handler: this._onKeyboardShortcut
}));
}
/**
* Unregisters the keyboard shortcut that toggles the audio muting.
*
* @inheritdoc
* @returns {void}
*/
override componentWillUnmount() {
this.props.dispatch(unregisterShortcut('M'));
}
/**
* Gets the current accessibility label, taking the toggled and GUM pending state into account. If no toggled label
* is provided, the regular accessibility label will also be used in the toggled state.
*
* The accessibility label is not visible in the UI, it is meant to be used by assistive technologies, mainly screen
* readers.
*
* @private
* @returns {string}
*/
override _getAccessibilityLabel() {
const { _gumPending } = this.props;
if (_gumPending === IGUMPendingState.NONE) {
return super._getAccessibilityLabel();
}
return 'toolbar.accessibilityLabel.muteGUMPending';
}
/**
* Gets the current label, taking the toggled and GUM pending state into account. If no
* toggled label is provided, the regular label will also be used in the toggled state.
*
* @private
* @returns {string}
*/
override _getLabel() {
const { _gumPending } = this.props;
if (_gumPending === IGUMPendingState.NONE) {
return super._getLabel();
}
return 'toolbar.muteGUMPending';
}
/**
* Indicates if audio is currently muted or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isAudioMuted() {
if (this.props._gumPending === IGUMPendingState.PENDING_UNMUTE) {
return false;
}
return super._isAudioMuted();
}
/**
* Creates an analytics keyboard shortcut event and dispatches an action to
* toggle the audio muting.
*
* @private
* @returns {void}
*/
_onKeyboardShortcut() {
// Ignore keyboard shortcuts if the audio button is disabled.
if (this._isDisabled()) {
return;
}
sendAnalytics(
createShortcutEvent(
AUDIO_MUTE,
ACTION_SHORTCUT_TRIGGERED,
{ enable: !this._isAudioMuted() }));
AbstractButton.prototype._onClick.call(this);
}
/**
* Returns a spinner if there is pending GUM.
*
* @returns {ReactElement | null}
*/
override _getElementAfter(): ReactElement | null {
const { _gumPending } = this.props;
const classes = withStyles.getClasses(this.props);
return _gumPending === IGUMPendingState.NONE ? null
: (
<div className = { classes.pendingContainer }>
<Spinner
color = { SPINNER_COLOR }
size = 'small' />
</div>
);
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code AudioMuteButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioMuted: boolean,
* _disabled: boolean
* }}
*/
function _mapStateToProps(state: IReduxState) {
const { gumPending } = state['features/base/media'].audio;
return {
...abstractMapStateToProps(state),
_gumPending: gumPending
};
}
export default withStyles(translate(connect(_mapStateToProps)(AudioMuteButton)), styles);

View File

@@ -0,0 +1,180 @@
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n/functions';
import { IconArrowUp } from '../../../base/icons/svg';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
import { IGUMPendingState } from '../../../base/media/types';
import ToolboxButtonWithIcon from '../../../base/toolbox/components/web/ToolboxButtonWithIcon';
import { toggleAudioSettings } from '../../../settings/actions.web';
import AudioSettingsPopup from '../../../settings/components/web/audio/AudioSettingsPopup';
import { getAudioSettingsVisibility } from '../../../settings/functions.web';
import { isAudioSettingsButtonDisabled } from '../../functions.web';
import AudioMuteButton from './AudioMuteButton';
interface IProps extends WithTranslation {
/**
* The button's key.
*/
buttonKey?: string;
/**
* The gumPending state from redux.
*/
gumPending: IGUMPendingState;
/**
* External handler for click action.
*/
handleClick: Function;
/**
* Indicates whether audio permissions have been granted or denied.
*/
hasPermissions: boolean;
/**
* If the button should be disabled.
*/
isDisabled: boolean;
/**
* Defines is popup is open.
*/
isOpen: boolean;
/**
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
*/
notifyMode?: string;
/**
* Click handler for the small icon. Opens audio options.
*/
onAudioOptionsClick: Function;
/**
* Flag controlling the visibility of the button.
* AudioSettings popup is disabled on mobile browsers.
*/
visible: boolean;
}
/**
* Button used for audio & audio settings.
*
* @returns {ReactElement}
*/
class AudioSettingsButton extends Component<IProps> {
/**
* Initializes a new {@code AudioSettingsButton} instance.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._onEscClick = this._onEscClick.bind(this);
this._onClick = this._onClick.bind(this);
}
/**
* Click handler for the more actions entries.
*
* @param {KeyboardEvent} event - Esc key click to close the popup.
* @returns {void}
*/
_onEscClick(event: React.KeyboardEvent) {
if (event.key === 'Escape' && this.props.isOpen) {
event.preventDefault();
event.stopPropagation();
this._onClick();
}
}
/**
* Click handler for the more actions entries.
*
* @param {MouseEvent} e - Mouse event.
* @returns {void}
*/
_onClick(e?: React.MouseEvent) {
const { onAudioOptionsClick, isOpen } = this.props;
if (isOpen) {
e?.stopPropagation();
}
onAudioOptionsClick();
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
override render() {
const { gumPending, hasPermissions, isDisabled, visible, isOpen, buttonKey, notifyMode, t } = this.props;
const settingsDisabled = !hasPermissions
|| isDisabled
|| !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported();
return visible ? (
<AudioSettingsPopup>
<ToolboxButtonWithIcon
ariaControls = 'audio-settings-dialog'
ariaExpanded = { isOpen }
ariaHasPopup = { true }
ariaLabel = { t('toolbar.audioSettings') }
buttonKey = { buttonKey }
icon = { IconArrowUp }
iconDisabled = { settingsDisabled || gumPending !== IGUMPendingState.NONE }
iconId = 'audio-settings-button'
iconTooltip = { t('toolbar.audioSettings') }
notifyMode = { notifyMode }
onIconClick = { this._onClick }
onIconKeyDown = { this._onEscClick }>
<AudioMuteButton
buttonKey = { buttonKey }
notifyMode = { notifyMode } />
</ToolboxButtonWithIcon>
</AudioSettingsPopup>
) : <AudioMuteButton
buttonKey = { buttonKey }
notifyMode = { notifyMode } />;
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
function mapStateToProps(state: IReduxState) {
const { permissions = { audio: false } } = state['features/base/devices'];
const { isNarrowLayout } = state['features/base/responsive-ui'];
const { gumPending } = state['features/base/media'].audio;
return {
gumPending,
hasPermissions: permissions.audio,
isDisabled: Boolean(isAudioSettingsButtonDisabled(state)),
isOpen: Boolean(getAudioSettingsVisibility(state)),
visible: !isMobileBrowser() && !isNarrowLayout
};
}
const mapDispatchToProps = {
onAudioOptionsClick: toggleAudioSettings
};
export default translate(connect(
mapStateToProps,
mapDispatchToProps
)(AudioSettingsButton));

View File

@@ -0,0 +1,40 @@
import React from 'react';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
interface IProps extends AbstractButtonProps {
backgroundColor?: string;
icon: string;
id?: string;
text: string;
}
/**
* Component that renders a custom toolbox button.
*
* @returns {Component}
*/
class CustomOptionButton extends AbstractButton<IProps> {
iconSrc = this.props.icon;
id = this.props.id;
text = this.props.text;
override backgroundColor = this.props.backgroundColor;
override accessibilityLabel = this.text;
/**
* Custom icon component.
*
* @param {any} props - Icon's props.
* @returns {img}
*/
override icon = (props: any) => (<img
src = { this.iconSrc }
{ ...props } />);
override label = this.text;
override tooltip = this.text;
}
export default CustomOptionButton;

View File

@@ -0,0 +1,126 @@
import { ReactNode, useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { debounce } from '../../../base/config/functions.any';
import { ZINDEX_DIALOG_PORTAL } from '../../constants';
interface IProps {
/**
* The component(s) to be displayed within the drawer portal.
*/
children: ReactNode;
/**
* Custom class name to apply on the container div.
*/
className?: string;
/**
* Function used to get the reference to the container div.
*/
getRef?: Function;
/**
* Function called when the portal target becomes actually visible.
*/
onVisible?: Function;
/**
* Function used to get the updated size info of the container on it's resize.
*/
setSize?: Function;
/**
* Custom style to apply to the container div.
*/
style?: any;
/**
* The selector for the element we consider the content container.
* This is used to determine the correct size of the portal content.
*/
targetSelector?: string;
}
/**
* Component meant to render a drawer at the bottom of the screen,
* by creating a portal containing the component's children.
*
* @returns {ReactElement}
*/
function DialogPortal({ children, className, style, getRef, setSize, targetSelector, onVisible }: IProps) {
const videoSpaceWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].videoSpaceWidth);
const [ portalTarget ] = useState(() => {
const portalDiv = document.createElement('div');
portalDiv.style.visibility = 'hidden';
return portalDiv;
});
const timerRef = useRef<number>();
useEffect(() => {
if (style) {
for (const styleProp of Object.keys(style)) {
const objStyle: any = portalTarget.style;
objStyle[styleProp] = style[styleProp];
}
}
if (className) {
portalTarget.className = className;
}
}, [ style, className ]);
useEffect(() => {
if (portalTarget && getRef) {
getRef(portalTarget);
portalTarget.style.zIndex = `${ZINDEX_DIALOG_PORTAL}`;
}
}, [ portalTarget, getRef ]);
useEffect(() => {
const size = {
width: 1,
height: 1
};
const debouncedResizeCallback = debounce((entries: ResizeObserverEntry[]) => {
const { contentRect } = entries[0];
if (contentRect.width !== size.width || contentRect.height !== size.height) {
setSize?.(contentRect);
clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(() => {
portalTarget.style.visibility = 'visible';
onVisible?.();
}, 100);
}
}, 20); // 20ms delay
// Create and observe ResizeObserver
const observer = new ResizeObserver(debouncedResizeCallback);
const target = targetSelector ? portalTarget.querySelector(targetSelector) : portalTarget;
if (document.body) {
document.body.appendChild(portalTarget);
observer.observe(target ?? portalTarget);
}
return () => {
observer.unobserve(target ?? portalTarget);
if (document.body) {
document.body.removeChild(portalTarget);
}
};
}, [ videoSpaceWidth ]);
return ReactDOM.createPortal(
children,
portalTarget
);
}
export default DialogPortal;

View File

@@ -0,0 +1,172 @@
import React, { KeyboardEvent, ReactNode, useCallback } from 'react';
import { FocusOn } from 'react-focus-on';
import { makeStyles } from 'tss-react/mui';
import { isElementInTheViewport } from '../../../base/ui/functions.web';
import { DRAWER_MAX_HEIGHT } from '../../constants';
interface IProps {
/**
* The component(s) to be displayed within the drawer menu.
*/
children: ReactNode;
/**
* Class name for custom styles.
*/
className?: string;
/**
* The id of the dom element acting as the Drawer label.
*/
headingId?: string;
/**
* Whether the drawer should be shown or not.
*/
isOpen: boolean;
/**
* Function that hides the drawer.
*/
onClose?: Function;
}
const useStyles = makeStyles()(theme => {
return {
drawerMenuContainer: {
backgroundColor: 'rgba(0,0,0,0.6)',
height: '100dvh',
display: 'flex',
alignItems: 'flex-end'
},
drawer: {
backgroundColor: theme.palette.ui01,
maxHeight: `calc(${DRAWER_MAX_HEIGHT})`,
borderRadius: '24px 24px 0 0',
overflowY: 'auto',
marginBottom: 'env(safe-area-inset-bottom, 0)',
width: '100%',
'& .overflow-menu': {
margin: 'auto',
fontSize: '1.2em',
listStyleType: 'none',
padding: 0,
height: 'calc(80vh - 144px - 64px)',
overflowY: 'auto',
'& .overflow-menu-item': {
boxSizing: 'border-box',
height: '48px',
padding: '12px 16px',
alignItems: 'center',
color: theme.palette.text01,
cursor: 'pointer',
display: 'flex',
fontSize: '1rem',
'& div': {
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
},
'&.disabled': {
cursor: 'initial',
color: '#3b475c'
}
}
}
}
};
});
/**
* Component that displays the mobile friendly drawer on web.
*
* @returns {ReactElement}
*/
function Drawer({
children,
className = '',
headingId,
isOpen,
onClose
}: IProps) {
const { classes, cx } = useStyles();
/**
* Handles clicks within the menu, preventing the propagation of the click event.
*
* @param {Object} event - The click event.
* @returns {void}
*/
const handleInsideClick = useCallback(event => {
event.stopPropagation();
}, []);
/**
* Handles clicks outside of the menu, closing it, and also stopping further propagation.
*
* @param {Object} event - The click event.
* @returns {void}
*/
const handleOutsideClick = useCallback(event => {
event.stopPropagation();
onClose?.();
}, [ onClose ]);
/**
* Handles pressing the escape key, closing the drawer.
*
* @param {KeyboardEvent<HTMLDivElement>} event - The keydown event.
* @returns {void}
*/
const handleEscKey = useCallback((event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
onClose?.();
}
}, [ onClose ]);
return (
isOpen ? (
<div
className = { classes.drawerMenuContainer }
onClick = { handleOutsideClick }
onKeyDown = { handleEscKey }>
<div
className = { cx(classes.drawer, className) }
onClick = { handleInsideClick }>
<FocusOn
returnFocus = {
// If we return the focus to an element outside the viewport the page will scroll to
// this element which in our case is undesirable and the element is outside of the
// viewport on purpose (to be hidden). For example if we return the focus to the toolbox
// when it is hidden the whole page will move up in order to show the toolbox. This is
// usually followed up with displaying the toolbox (because now it is on focus) but
// because of the animation the whole scenario looks like jumping large video.
isElementInTheViewport
}>
<div
aria-labelledby = { headingId ? `#${headingId}` : undefined }
aria-modal = { true }
data-autofocus = { true }
role = 'dialog'
tabIndex = { -1 }>
{children}
</div>
</FocusOn>
</div>
</div>
) : null
);
}
export default Drawer;

View File

@@ -0,0 +1,55 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { endConference } from '../../../base/conference/actions';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { HangupContextMenuItem } from './HangupContextMenuItem';
/**
* The type of the React {@code Component} props of {@link EndConferenceButton}.
*/
interface IProps {
/**
* Key to use for toolbarButtonClicked event.
*/
buttonKey: string;
/**
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
*/
notifyMode?: string;
}
/**
* Button to end the conference for all participants.
*
* @param {Object} props - Component's props.
* @returns {JSX.Element} - The end conference button.
*/
export const EndConferenceButton = (props: IProps) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const _isLocalParticipantModerator = useSelector(isLocalParticipantModerator);
const _isInBreakoutRoom = useSelector(isInBreakoutRoom);
const onEndConference = useCallback(() => {
dispatch(endConference());
}, [ dispatch ]);
return (<>
{ !_isInBreakoutRoom && _isLocalParticipantModerator && <HangupContextMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.endConference') }
buttonKey = { props.buttonKey }
buttonType = { BUTTON_TYPES.DESTRUCTIVE }
label = { t('toolbar.endConference') }
notifyMode = { props.notifyMode }
onClick = { onEndConference } /> }
</>);
};

View File

@@ -0,0 +1,77 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { isIosMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n/functions';
import { IconEnterFullscreen, IconExitFullscreen } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { closeOverflowMenuIfOpen, setFullScreen } from '../../actions.web';
interface IProps extends AbstractButtonProps {
/**
* Whether or not the app is currently in full screen.
*/
_fullScreen?: boolean;
}
/**
* Implementation of a button for toggling fullscreen state.
*/
class FullscreenButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.enterFullScreen';
override toggledAccessibilityLabel = 'toolbar.accessibilityLabel.exitFullScreen';
override label = 'toolbar.enterFullScreen';
override toggledLabel = 'toolbar.exitFullScreen';
override tooltip = 'toolbar.enterFullScreen';
override toggledTooltip = 'toolbar.exitFullScreen';
override toggledIcon = IconExitFullscreen;
override icon = IconEnterFullscreen;
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._fullScreen;
}
/**
* Handles clicking the button, and toggles fullscreen.
*
* @private
* @returns {void}
*/
override _handleClick() {
const { dispatch, _fullScreen } = this.props;
sendAnalytics(createToolbarEvent(
'toggle.fullscreen',
{
enable: !_fullScreen
}));
dispatch(closeOverflowMenuIfOpen());
dispatch(setFullScreen(!_fullScreen));
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
const mapStateToProps = (state: IReduxState) => {
return {
_fullScreen: state['features/toolbox'].fullScreen,
visible: !isIosMobileBrowser()
};
};
export default translate(connect(mapStateToProps)(FullscreenButton));

View File

@@ -0,0 +1,73 @@
import React, { useCallback } from 'react';
import Button from '../../../base/ui/components/web/Button';
import { NOTIFY_CLICK_MODE } from '../../types';
/**
* The type of the React {@code Component} props of {@link HangupContextMenuItem}.
*/
interface IProps {
/**
* Accessibility label for the button.
*/
accessibilityLabel: string;
/**
* Key to use for toolbarButtonClicked event.
*/
buttonKey: string;
/**
* Type of button to display.
*/
buttonType: string;
/**
* Text associated with the button.
*/
label: string;
/**
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
*/
notifyMode?: string;
/**
* Callback that performs the actual hangup action.
*/
onClick: Function;
}
/**
* Implementation of a button to be rendered within Hangup context menu.
*
* @param {Object} props - Component's props.
* @returns {JSX.Element} - Button that would trigger the hangup action.
*/
export const HangupContextMenuItem = (props: IProps) => {
const shouldNotify = props.notifyMode !== undefined;
const shouldPreventExecution = props.notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY;
const _onClick = useCallback(() => {
if (shouldNotify) {
APP.API.notifyToolbarButtonClicked(props.buttonKey, shouldPreventExecution);
}
if (!shouldPreventExecution) {
props.onClick();
}
}, []);
return (
<Button
accessibilityLabel = { props.accessibilityLabel }
fullWidth = { true }
label = { props.label }
onClick = { _onClick }
type = { props.buttonType } />
);
};

View File

@@ -0,0 +1,134 @@
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { translate } from '../../../base/i18n/functions';
import Popover from '../../../base/popover/components/Popover.web';
import HangupToggleButton from './HangupToggleButton';
/**
* The type of the React {@code Component} props of {@link HangupMenuButton}.
*/
interface IProps extends WithTranslation {
/**
* ID of the menu that is controlled by this button.
*/
ariaControls: String;
/**
* A child React Element to display within {@code InlineDialog}.
*/
children: React.ReactNode;
/**
* Whether or not the HangupMenu popover should display.
*/
isOpen: boolean;
/**
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
*/
notifyMode?: string;
/**
* Callback to change the visibility of the hangup menu.
*/
onVisibilityChange: Function;
}
/**
* A React {@code Component} for opening or closing the {@code HangupMenu}.
*
* @augments Component
*/
class HangupMenuButton extends Component<IProps> {
/**
* Initializes a new {@code HangupMenuButton} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: IProps) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onCloseDialog = this._onCloseDialog.bind(this);
this._toggleDialogVisibility
= this._toggleDialogVisibility.bind(this);
this._onEscClick = this._onEscClick.bind(this);
}
/**
* Click handler for the more actions entries.
*
* @param {KeyboardEvent} event - Esc key click to close the popup.
* @returns {void}
*/
_onEscClick(event: KeyboardEvent) {
if (event.key === 'Escape' && this.props.isOpen) {
event.preventDefault();
event.stopPropagation();
this._onCloseDialog();
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
const { children, isOpen, t } = this.props;
return (
<div className = 'toolbox-button-wth-dialog context-menu'>
<Popover
content = { children }
headingLabel = { t('toolbar.accessibilityLabel.hangup') }
onPopoverClose = { this._onCloseDialog }
position = 'top'
trigger = 'click'
visible = { isOpen }>
<HangupToggleButton
buttonKey = 'hangup-menu'
customClass = 'hangup-menu-button'
handleClick = { this._toggleDialogVisibility }
isOpen = { isOpen }
notifyMode = { this.props.notifyMode }
onKeyDown = { this._onEscClick } />
</Popover>
</div>
);
}
/**
* Callback invoked when {@code InlineDialog} signals that it should be
* close.
*
* @private
* @returns {void}
*/
_onCloseDialog() {
this.props.onVisibilityChange(false);
}
/**
* Callback invoked to signal that an event has occurred that should change
* the visibility of the {@code InlineDialog} component.
*
* @private
* @returns {void}
*/
_toggleDialogVisibility() {
sendAnalytics(createToolbarEvent('hangup'));
this.props.onVisibilityChange(!this.props.isOpen);
}
}
export default translate(HangupMenuButton);

View File

@@ -0,0 +1,57 @@
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import { IconCloseLarge, IconHangup } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
/**
* The type of the React {@code Component} props of {@link HangupToggleButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether the more options menu is open.
*/
isOpen: boolean;
/**
* External handler for key down action.
*/
onKeyDown: Function;
}
/**
* Implementation of a button for toggling the hangup menu.
*/
class HangupToggleButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.hangup';
override icon = IconHangup;
override label = 'toolbar.hangup';
override toggledIcon = IconCloseLarge;
override toggledLabel = 'toolbar.hangup';
override tooltip = 'toolbar.hangup';
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props.isOpen;
}
/**
* Indicates whether a key was pressed.
*
* @override
* @protected
* @returns {boolean}
*/
override _onKeyDown() {
this.props.onKeyDown();
}
}
export default connect()(translate(HangupToggleButton));

View File

@@ -0,0 +1,58 @@
import React, { ReactNode } from 'react';
import { makeStyles } from 'tss-react/mui';
import DialogPortal from './DialogPortal';
interface IProps {
/**
* The component(s) to be displayed within the drawer portal.
*/
children: ReactNode;
/**
* Class name used to add custom styles to the portal.
*/
className?: string;
}
const useStyles = makeStyles()(theme => {
return {
portal: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
zIndex: 351,
borderRadius: '16px 16px 0 0',
'&.notification-portal': {
zIndex: 901
},
'&::after': {
content: '""',
backgroundColor: theme.palette.ui01,
marginBottom: 'env(safe-area-inset-bottom, 0)'
}
}
};
});
/**
* Component meant to render a drawer at the bottom of the screen,
* by creating a portal containing the component's children.
*
* @returns {ReactElement}
*/
function JitsiPortal({ children, className }: IProps) {
const { classes, cx } = useStyles();
return (
<DialogPortal className = { cx(classes.portal, className) }>
{ children }
</DialogPortal>
);
}
export default JitsiPortal;

View File

@@ -0,0 +1,54 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { leaveConference } from '../../../base/conference/actions';
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
import { HangupContextMenuItem } from './HangupContextMenuItem';
/**
* The type of the React {@code Component} props of {@link LeaveConferenceButton}.
*/
interface IProps {
/**
* Key to use for toolbarButtonClicked event.
*/
buttonKey: string;
/**
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
*/
notifyMode?: string;
}
/**
* Button to leave the conference.
*
* @param {Object} props - Component's props.
* @returns {JSX.Element} - The leave conference button.
*/
export const LeaveConferenceButton = (props: IProps) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const onLeaveConference = useCallback(() => {
sendAnalytics(createToolbarEvent('hangup'));
dispatch(leaveConference());
}, [ dispatch ]);
return (
<HangupContextMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.leaveConference') }
buttonKey = { props.buttonKey }
buttonType = { BUTTON_TYPES.SECONDARY }
label = { t('toolbar.leaveConference') }
notifyMode = { props.notifyMode }
onClick = { onLeaveConference } />
);
};

View File

@@ -0,0 +1,48 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { openDialog } from '../../../base/dialog/actions';
import { translate } from '../../../base/i18n/functions';
import { IconCloudUpload } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import SalesforceLinkDialog from '../../../salesforce/components/web/SalesforceLinkDialog';
import { isSalesforceEnabled } from '../../../salesforce/functions';
/**
* Implementation of a button for opening the Salesforce link dialog.
*/
class LinkToSalesforce extends AbstractButton<AbstractButtonProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.linkToSalesforce';
override icon = IconCloudUpload;
override label = 'toolbar.linkToSalesforce';
override tooltip = 'toolbar.linkToSalesforce';
/**
* Handles clicking / pressing the button, and opens the Salesforce link dialog.
*
* @protected
* @returns {void}
*/
override _handleClick() {
const { dispatch } = this.props;
sendAnalytics(createToolbarEvent('link.to.salesforce'));
dispatch(openDialog(SalesforceLinkDialog));
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
const mapStateToProps = (state: IReduxState) => {
return {
visible: isSalesforceEnabled(state)
};
};
export default translate(connect(mapStateToProps)(LinkToSalesforce));

View File

@@ -0,0 +1,262 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import Popover from '../../../base/popover/components/Popover.web';
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
import { setGifMenuVisibility } from '../../../gifs/actions';
import { isGifsMenuOpen } from '../../../gifs/functions.web';
import ReactionEmoji from '../../../reactions/components/web/ReactionEmoji';
import ReactionsMenu from '../../../reactions/components/web/ReactionsMenu';
import {
GIFS_MENU_HEIGHT_IN_OVERFLOW_MENU,
RAISE_HAND_ROW_HEIGHT,
REACTIONS_MENU_HEIGHT_DRAWER,
REACTIONS_MENU_HEIGHT_IN_OVERFLOW_MENU
} from '../../../reactions/constants';
import { getReactionsQueue } from '../../../reactions/functions.any';
import { IReactionsMenuParent } from '../../../reactions/types';
import { DRAWER_MAX_HEIGHT } from '../../constants';
import { showOverflowDrawer } from '../../functions.web';
import Drawer from './Drawer';
import JitsiPortal from './JitsiPortal';
import OverflowToggleButton from './OverflowToggleButton';
/**
* The type of the React {@code Component} props of {@link OverflowMenuButton}.
*/
interface IProps {
/**
* ID of the menu that is controlled by this button.
*/
ariaControls: string;
/**
* Information about the buttons that need to be rendered in the overflow menu.
*/
buttons: Object[];
/**
* Whether or not the OverflowMenu popover should display.
*/
isOpen: boolean;
/**
* Esc key handler.
*/
onToolboxEscKey: (e?: React.KeyboardEvent) => void;
/**
* Callback to change the visibility of the overflow menu.
*/
onVisibilityChange: Function;
/**
* Whether to show the raise hand in the reactions menu or not.
*/
showRaiseHandInReactionsMenu: boolean;
/**
* Whether or not to display the reactions menu.
*/
showReactionsMenu: boolean;
}
const useStyles = makeStyles<{ overflowDrawer: boolean; reactionsMenuHeight: number; }>()(
(_theme, { reactionsMenuHeight, overflowDrawer }) => {
return {
overflowMenuDrawer: {
overflowY: 'scroll',
height: `calc(${DRAWER_MAX_HEIGHT})`
},
contextMenu: {
position: 'relative' as const,
right: 'auto',
margin: 0,
marginBottom: '8px',
maxHeight: overflowDrawer ? undefined : 'calc(100dvh - 100px)',
paddingBottom: overflowDrawer ? undefined : 0,
minWidth: '240px',
overflow: 'hidden'
},
content: {
position: 'relative',
maxHeight: overflowDrawer
? `calc(100% - ${reactionsMenuHeight}px - 16px)` : `calc(100dvh - 100px - ${reactionsMenuHeight}px)`,
overflowY: 'auto'
},
footer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0
},
reactionsPadding: {
height: `${reactionsMenuHeight}px`
}
};
});
const OverflowMenuButton = ({
buttons,
isOpen,
onToolboxEscKey,
onVisibilityChange,
showRaiseHandInReactionsMenu,
showReactionsMenu
}: IProps) => {
const overflowDrawer = useSelector(showOverflowDrawer);
const reactionsQueue = useSelector(getReactionsQueue);
const isGiphyVisible = useSelector(isGifsMenuOpen);
const dispatch = useDispatch();
const onCloseDialog = useCallback(() => {
onVisibilityChange(false);
if (isGiphyVisible && !overflowDrawer) {
dispatch(setGifMenuVisibility(false));
}
}, [ onVisibilityChange, setGifMenuVisibility, isGiphyVisible, overflowDrawer, dispatch ]);
const onOpenDialog = useCallback(() => {
onVisibilityChange(true);
}, [ onVisibilityChange ]);
const onEscClick = useCallback((event: React.KeyboardEvent) => {
if (event.key === 'Escape' && isOpen) {
event.preventDefault();
event.stopPropagation();
onCloseDialog();
}
}, [ onCloseDialog ]);
const toggleDialogVisibility = useCallback(() => {
sendAnalytics(createToolbarEvent('overflow'));
onVisibilityChange(!isOpen);
}, [ isOpen, onVisibilityChange ]);
const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu';
const { t } = useTranslation();
let reactionsMenuHeight = 0;
if (showReactionsMenu) {
reactionsMenuHeight = REACTIONS_MENU_HEIGHT_DRAWER;
if (!overflowDrawer) {
reactionsMenuHeight = REACTIONS_MENU_HEIGHT_IN_OVERFLOW_MENU;
}
if (!showRaiseHandInReactionsMenu) {
reactionsMenuHeight -= RAISE_HAND_ROW_HEIGHT;
}
if (!overflowDrawer && isGiphyVisible) {
reactionsMenuHeight += GIFS_MENU_HEIGHT_IN_OVERFLOW_MENU;
}
}
const { classes } = useStyles({
reactionsMenuHeight,
overflowDrawer
});
const groupsJSX = buttons.map((buttonGroup: any) => (
<ContextMenuItemGroup key = { `group-${buttonGroup[0].group}` }>
{buttonGroup.map(({ key, Content, ...rest }: { Content: React.ElementType; key: string; }) => {
const props: { buttonKey?: string; contextMenu?: boolean; showLabel?: boolean; } = { ...rest };
if (key !== 'reactions') {
props.buttonKey = key;
props.contextMenu = true;
props.showLabel = true;
}
return (
<Content
{ ...props }
key = { key } />);
})}
</ContextMenuItemGroup>));
const overflowMenu = groupsJSX && (
<ContextMenu
accessibilityLabel = { t(toolbarAccLabel) }
className = { classes.contextMenu }
hidden = { false }
id = 'overflow-context-menu'
inDrawer = { overflowDrawer }
onKeyDown = { onToolboxEscKey }>
<div className = { classes.content }>
{ groupsJSX }
</div>
{
showReactionsMenu && (<div className = { classes.footer }>
<ReactionsMenu
parent = {
overflowDrawer ? IReactionsMenuParent.OverflowDrawer : IReactionsMenuParent.OverflowMenu }
showRaisedHand = { showRaiseHandInReactionsMenu } />
</div>)
}
</ContextMenu>);
if (overflowDrawer) {
return (
<div className = 'toolbox-button-wth-dialog context-menu'>
<>
<OverflowToggleButton
handleClick = { toggleDialogVisibility }
isOpen = { isOpen }
onKeyDown = { onEscClick } />
<JitsiPortal>
<Drawer
isOpen = { isOpen }
onClose = { onCloseDialog }>
<>
<div className = { classes.overflowMenuDrawer }>
{ overflowMenu }
<div className = { classes.reactionsPadding } />
</div>
</>
</Drawer>
{showReactionsMenu && <div className = 'reactions-animations-overflow-container'>
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
index = { index }
key = { uid }
reaction = { reaction }
uid = { uid } />))}
</div>}
</JitsiPortal>
</>
</div>
);
}
return (
<div className = 'toolbox-button-wth-dialog context-menu'>
<Popover
content = { overflowMenu }
headingId = 'overflow-context-menu'
onPopoverClose = { onCloseDialog }
onPopoverOpen = { onOpenDialog }
position = 'top'
trigger = 'click'
visible = { isOpen }>
<OverflowToggleButton
isMenuButton = { true }
isOpen = { isOpen }
onKeyDown = { onEscClick } />
</Popover>
{showReactionsMenu && <div className = 'reactions-animations-container'>
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
index = { index }
key = { uid }
reaction = { reaction }
uid = { uid } />))}
</div>}
</div>
);
};
export default OverflowMenuButton;

View File

@@ -0,0 +1,59 @@
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import { IconDotsHorizontal } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
/**
* The type of the React {@code Component} props of {@link OverflowToggleButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether the more options menu is open.
*/
isOpen: boolean;
/**
* External handler for key down action.
*/
onKeyDown: Function;
}
/**
* Implementation of a button for toggling the overflow menu.
*/
class OverflowToggleButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.moreActions';
override toggledAccessibilityLabel = 'toolbar.accessibilityLabel.closeMoreActions';
override icon = IconDotsHorizontal;
override label = 'toolbar.moreActions';
override toggledLabel = 'toolbar.moreActions';
override tooltip = 'toolbar.moreActions';
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props.isOpen;
}
/**
* Indicates whether a key was pressed.
*
* @override
* @protected
* @returns {boolean}
*/
override _onKeyDown() {
this.props.onKeyDown();
}
}
export default connect()(translate(OverflowToggleButton));

View File

@@ -0,0 +1,116 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { getLocalParticipant } from '../../../base/participants/functions';
import { ILocalParticipant } from '../../../base/participants/types';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { openSettingsDialog } from '../../../settings/actions';
import { SETTINGS_TABS } from '../../../settings/constants';
import ProfileButtonAvatar from './ProfileButtonAvatar';
/**
* The type of the React {@code Component} props of {@link ProfileButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Default displayed name for local participant.
*/
_defaultLocalDisplayName: string;
/**
* The redux representation of the local participant.
*/
_localParticipant?: ILocalParticipant;
/**
* Whether the button support clicking or not.
*/
_unclickable: boolean;
}
/**
* Implementation of a button for opening profile dialog.
*/
class ProfileButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.profile';
override icon = ProfileButtonAvatar;
/**
* Retrieves the label.
*
* @returns {string}
*/
override _getLabel() {
const {
_defaultLocalDisplayName,
_localParticipant
} = this.props;
let displayName;
if (_localParticipant?.name) {
displayName = _localParticipant.name;
} else {
displayName = _defaultLocalDisplayName;
}
return displayName;
}
/**
* Retrieves the tooltip.
*
* @returns {string}
*/
override _getTooltip() {
return this._getLabel();
}
/**
* Handles clicking / pressing the button, and opens the appropriate dialog.
*
* @protected
* @returns {void}
*/
override _handleClick() {
const { dispatch, _unclickable } = this.props;
if (!_unclickable) {
sendAnalytics(createToolbarEvent('profile'));
dispatch(openSettingsDialog(SETTINGS_TABS.PROFILE));
}
}
/**
* Indicates whether the button should be disabled or not.
*
* @protected
* @returns {void}
*/
override _isDisabled() {
return this.props._unclickable;
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
const mapStateToProps = (state: IReduxState) => {
const { defaultLocalDisplayName } = state['features/base/config'];
return {
_defaultLocalDisplayName: defaultLocalDisplayName ?? '',
_localParticipant: getLocalParticipant(state),
_unclickable: !interfaceConfig.SETTINGS_SECTIONS.includes('profile'),
customClass: 'profile-button-avatar'
};
};
export default translate(connect(mapStateToProps)(ProfileButton));

View File

@@ -0,0 +1,62 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import Avatar from '../../../base/avatar/components/Avatar';
import { getLocalParticipant } from '../../../base/participants/functions';
import { ILocalParticipant } from '../../../base/participants/types';
/**
* The type of the React {@code Component} props of
* {@link ProfileButtonAvatar}.
*/
interface IProps {
/**
* The redux representation of the local participant.
*/
_localParticipant?: ILocalParticipant;
}
/**
* A React {@code Component} for displaying a profile avatar as an
* icon.
*
* @augments Component
*/
class ProfileButtonAvatar extends Component<IProps> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
const { _localParticipant } = this.props;
return (
<Avatar
participantId = { _localParticipant?.id }
size = { 20 } />
);
}
}
/**
* Maps (parts of) the Redux state to the associated
* {@code ProfileButtonAvatar} component's props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _localParticipant: Object,
* }}
*/
function _mapStateToProps(state: IReduxState) {
return {
_localParticipant: getLocalParticipant(state)
};
}
export default connect(_mapStateToProps)(ProfileButtonAvatar);

View File

@@ -0,0 +1,3 @@
import React from 'react';
export default () => <hr className = 'overflow-menu-hr' />;

View File

@@ -0,0 +1,116 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconScreenshare } from '../../../base/icons/svg';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { startScreenShareFlow } from '../../../screen-share/actions.web';
import { isScreenVideoShared } from '../../../screen-share/functions';
import { closeOverflowMenuIfOpen } from '../../actions.web';
import { isDesktopShareButtonDisabled } from '../../functions.web';
interface IProps extends AbstractButtonProps {
/**
* Whether or not screen-sharing is initialized.
*/
_desktopSharingEnabled: boolean;
/**
* Whether or not the local participant is screen-sharing.
*/
_screensharing: boolean;
}
/**
* Implementation of a button for sharing desktop / windows.
*/
class ShareDesktopButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.shareYourScreen';
override toggledAccessibilityLabel = 'toolbar.accessibilityLabel.stopScreenSharing';
override label = 'toolbar.startScreenSharing';
override icon = IconScreenshare;
override toggledLabel = 'toolbar.stopScreenSharing';
/**
* Retrieves tooltip dynamically.
*
* @returns {string}
*/
override _getTooltip() {
const { _desktopSharingEnabled, _screensharing } = this.props;
if (_desktopSharingEnabled) {
if (_screensharing) {
return 'toolbar.stopScreenSharing';
}
return 'toolbar.startScreenSharing';
}
return 'dialog.shareYourScreenDisabled';
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._screensharing;
}
/**
* Indicates whether this button is in disabled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isDisabled() {
return !this.props._desktopSharingEnabled;
}
/**
* Handles clicking the button, and toggles the chat.
*
* @private
* @returns {void}
*/
override _handleClick() {
const { dispatch, _screensharing } = this.props;
sendAnalytics(createToolbarEvent(
'toggle.screen.sharing',
{ enable: !_screensharing }));
dispatch(closeOverflowMenuIfOpen());
dispatch(startScreenShareFlow(!_screensharing));
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
const mapStateToProps = (state: IReduxState) => {
// Disable the screen-share button if the video sender limit is reached and there is no video or media share in
// progress.
const desktopSharingEnabled
= JitsiMeetJS.isDesktopSharingEnabled() && !isDesktopShareButtonDisabled(state);
return {
_desktopSharingEnabled: desktopSharingEnabled,
_screensharing: isScreenVideoShared(state),
visible: JitsiMeetJS.isDesktopSharingEnabled()
};
};
export default translate(connect(mapStateToProps)(ShareDesktopButton));

View File

@@ -0,0 +1,76 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconCameraRefresh } from '../../../base/icons/svg';
import { MEDIA_TYPE } from '../../../base/media/constants';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { toggleCamera } from '../../../base/tracks/actions';
import { isLocalTrackMuted, isToggleCameraEnabled } from '../../../base/tracks/functions';
import { setOverflowMenuVisible } from '../../actions.web';
/**
* The type of the React {@code Component} props of {@link ToggleCameraButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether the current conference is in audio only mode or not.
*/
_audioOnly: boolean;
/**
* Whether video is currently muted or not.
*/
_videoMuted: boolean;
}
/**
* An implementation of a button for toggling the camera facing mode.
*/
class ToggleCameraButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.toggleCamera';
override icon = IconCameraRefresh;
override label = 'toolbar.toggleCamera';
/**
* Handles clicking/pressing the button.
*
* @returns {void}
*/
override _handleClick() {
const { dispatch } = this.props;
dispatch(toggleCamera());
dispatch(setOverflowMenuVisible(false));
}
/**
* Whether this button is disabled or not.
*
* @returns {boolean}
*/
override _isDisabled() {
return this.props._audioOnly || this.props._videoMuted;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ToggleCameraButton} component.
*
* @param {Object} state - The Redux state.
* @returns {IProps}
*/
function mapStateToProps(state: IReduxState) {
const { enabled: audioOnly } = state['features/base/audio-only'];
const tracks = state['features/base/tracks'];
return {
_audioOnly: Boolean(audioOnly),
_videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO),
visible: isToggleCameraEnabled(state)
};
}
export default translate(connect(mapStateToProps)(ToggleCameraButton));

View File

@@ -0,0 +1,334 @@
import React, { useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import { isMobileBrowser } from '../../../base/environment/utils';
import { getLocalParticipant, isLocalParticipantModerator } from '../../../base/participants/functions';
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
import { isReactionsButtonEnabled, shouldDisplayReactionsButtons } from '../../../reactions/functions.web';
import { isCCTabEnabled } from '../../../subtitles/functions.any';
import { isTranscribing } from '../../../transcribing/functions';
import {
setHangupMenuVisible,
setOverflowMenuVisible,
setToolbarHovered,
setToolboxVisible
} from '../../actions.web';
import {
getJwtDisabledButtons,
getVisibleButtons,
isButtonEnabled,
isToolboxVisible
} from '../../functions.web';
import { useKeyboardShortcuts, useToolboxButtons } from '../../hooks.web';
import { IToolboxButton } from '../../types';
import HangupButton from '../HangupButton';
import { EndConferenceButton } from './EndConferenceButton';
import HangupMenuButton from './HangupMenuButton';
import { LeaveConferenceButton } from './LeaveConferenceButton';
import OverflowMenuButton from './OverflowMenuButton';
import Separator from './Separator';
/**
* The type of the React {@code Component} props of {@link Toolbox}.
*/
interface IProps {
/**
* Explicitly passed array with the buttons which this Toolbox should display.
*/
toolbarButtons?: Array<string>;
}
const useStyles = makeStyles()(() => {
return {
hangupMenu: {
position: 'relative',
right: 'auto',
display: 'flex',
flexDirection: 'column',
rowGap: '8px',
margin: 0,
padding: '16px',
marginBottom: '8px'
}
};
});
/**
* A component that renders the main toolbar.
*
* @param {IProps} props - The props of the component.
* @returns {ReactElement}
*/
export default function Toolbox({
toolbarButtons
}: IProps) {
const { classes, cx } = useStyles();
const { t } = useTranslation();
const dispatch = useDispatch();
const _toolboxRef = useRef<HTMLDivElement>(null);
const conference = useSelector((state: IReduxState) => state['features/base/conference'].conference);
const isNarrowLayout = useSelector((state: IReduxState) => state['features/base/responsive-ui'].isNarrowLayout);
const videoSpaceWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].videoSpaceWidth);
const isModerator = useSelector(isLocalParticipantModerator);
const customToolbarButtons = useSelector(
(state: IReduxState) => state['features/base/config'].customToolbarButtons);
const iAmRecorder = useSelector((state: IReduxState) => state['features/base/config'].iAmRecorder);
const iAmSipGateway = useSelector((state: IReduxState) => state['features/base/config'].iAmSipGateway);
const overflowDrawer = useSelector((state: IReduxState) => state['features/toolbox'].overflowDrawer);
const shiftUp = useSelector((state: IReduxState) => state['features/toolbox'].shiftUp);
const overflowMenuVisible = useSelector((state: IReduxState) => state['features/toolbox'].overflowMenuVisible);
const hangupMenuVisible = useSelector((state: IReduxState) => state['features/toolbox'].hangupMenuVisible);
const buttonsWithNotifyClick
= useSelector((state: IReduxState) => state['features/toolbox'].buttonsWithNotifyClick);
const reduxToolbarButtons = useSelector((state: IReduxState) => state['features/toolbox'].toolbarButtons);
const toolbarButtonsToUse = toolbarButtons || reduxToolbarButtons;
const isDialogVisible = useSelector((state: IReduxState) => Boolean(state['features/base/dialog'].component));
const localParticipant = useSelector(getLocalParticipant);
const transcribing = useSelector(isTranscribing);
const _isCCTabEnabled = useSelector(isCCTabEnabled);
// Do not convert to selector, it returns new array and will cause re-rendering of toolbox on every action.
const jwtDisabledButtons = getJwtDisabledButtons(transcribing, _isCCTabEnabled, localParticipant?.features);
const reactionsButtonEnabled = useSelector(isReactionsButtonEnabled);
const _shouldDisplayReactionsButtons = useSelector(shouldDisplayReactionsButtons);
const toolbarVisible = useSelector(isToolboxVisible);
const mainToolbarButtonsThresholds
= useSelector((state: IReduxState) => state['features/toolbox'].mainToolbarButtonsThresholds);
const allButtons = useToolboxButtons(customToolbarButtons);
const isMobile = isMobileBrowser();
const endConferenceSupported = Boolean(conference?.isEndConferenceSupported() && isModerator);
useKeyboardShortcuts(toolbarButtonsToUse);
useEffect(() => {
if (!toolbarVisible) {
if (document.activeElement instanceof HTMLElement
&& _toolboxRef.current?.contains(document.activeElement)) {
document.activeElement.blur();
}
}
}, [ toolbarVisible ]);
/**
* Sets the visibility of the hangup menu.
*
* @param {boolean} visible - Whether or not the hangup menu should be
* displayed.
* @private
* @returns {void}
*/
const onSetHangupVisible = useCallback((visible: boolean) => {
dispatch(setHangupMenuVisible(visible));
dispatch(setToolbarHovered(visible));
}, [ dispatch ]);
/**
* Sets the visibility of the overflow menu.
*
* @param {boolean} visible - Whether or not the overflow menu should be
* displayed.
* @private
* @returns {void}
*/
const onSetOverflowVisible = useCallback((visible: boolean) => {
dispatch(setOverflowMenuVisible(visible));
dispatch(setToolbarHovered(visible));
}, [ dispatch ]);
useEffect(() => {
// On mobile web we want to keep both toolbox and hang up menu visible
// because they depend on each other.
if (endConferenceSupported && isMobile) {
hangupMenuVisible && dispatch(setToolboxVisible(true));
} else if (hangupMenuVisible && !toolbarVisible) {
onSetHangupVisible(false);
dispatch(setToolbarHovered(false));
}
}, [ dispatch, hangupMenuVisible, toolbarVisible, onSetHangupVisible ]);
useEffect(() => {
if (overflowMenuVisible && isDialogVisible) {
onSetOverflowVisible(false);
dispatch(setToolbarHovered(false));
}
}, [ dispatch, overflowMenuVisible, isDialogVisible, onSetOverflowVisible ]);
/**
* Key handler for overflow/hangup menus.
*
* @param {KeyboardEvent} e - Esc key click to close the popup.
* @returns {void}
*/
const onEscKey = useCallback((e?: React.KeyboardEvent) => {
if (e?.key === 'Escape') {
e?.stopPropagation();
hangupMenuVisible && dispatch(setHangupMenuVisible(false));
overflowMenuVisible && dispatch(setOverflowMenuVisible(false));
}
}, [ dispatch, hangupMenuVisible, overflowMenuVisible ]);
/**
* Dispatches an action signaling the toolbar is not being hovered.
*
* @private
* @returns {void}
*/
const onMouseOut = useCallback(() => {
!overflowMenuVisible && dispatch(setToolbarHovered(false));
}, [ dispatch, overflowMenuVisible ]);
/**
* Dispatches an action signaling the toolbar is being hovered.
*
* @private
* @returns {void}
*/
const onMouseOver = useCallback(() => {
dispatch(setToolbarHovered(true));
}, [ dispatch ]);
/**
* Handle focus on the toolbar.
*
* @returns {void}
*/
const handleFocus = useCallback(() => {
dispatch(setToolboxVisible(true));
}, [ dispatch ]);
/**
* Handle blur the toolbar..
*
* @returns {void}
*/
const handleBlur = useCallback(() => {
dispatch(setToolboxVisible(false));
}, [ dispatch ]);
if (iAmRecorder || iAmSipGateway) {
return null;
}
const rootClassNames = `new-toolbox ${toolbarVisible ? 'visible' : ''} ${
toolbarButtonsToUse.length ? '' : 'no-buttons'}`;
const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu';
const containerClassName = `toolbox-content${isMobile || isNarrowLayout ? ' toolbox-content-mobile' : ''}`;
const { mainMenuButtons, overflowMenuButtons } = getVisibleButtons({
allButtons,
buttonsWithNotifyClick,
toolbarButtons: toolbarButtonsToUse,
clientWidth: videoSpaceWidth,
jwtDisabledButtons,
mainToolbarButtonsThresholds
});
const raiseHandInOverflowMenu = overflowMenuButtons.some(({ key }) => key === 'raisehand');
const showReactionsInOverflowMenu = _shouldDisplayReactionsButtons
&& (
(!reactionsButtonEnabled && (raiseHandInOverflowMenu || isNarrowLayout || isMobile))
|| overflowMenuButtons.some(({ key }) => key === 'reactions'));
const showRaiseHandInReactionsMenu = showReactionsInOverflowMenu && raiseHandInOverflowMenu;
return (
<div
className = { cx(rootClassNames, shiftUp && 'shift-up') }
id = 'new-toolbox'>
<div className = { containerClassName }>
<div
className = 'toolbox-content-wrapper'
onBlur = { handleBlur }
onFocus = { handleFocus }
{ ...(isMobile ? {} : {
onMouseOut,
onMouseOver
}) }>
<div
className = 'toolbox-content-items'
ref = { _toolboxRef }>
{mainMenuButtons.map(({ Content, key, ...rest }) => Content !== Separator && (
<Content
{ ...rest }
buttonKey = { key }
key = { key } />))}
{Boolean(overflowMenuButtons.length) && (
<OverflowMenuButton
ariaControls = 'overflow-menu'
buttons = { overflowMenuButtons.reduce<Array<IToolboxButton[]>>((acc, val) => {
if (val.key === 'reactions' && showReactionsInOverflowMenu) {
return acc;
}
if (val.key === 'raisehand' && showRaiseHandInReactionsMenu) {
return acc;
}
if (acc.length) {
const prev = acc[acc.length - 1];
const group = prev[prev.length - 1].group;
if (group === val.group) {
prev.push(val);
} else {
acc.push([ val ]);
}
} else {
acc.push([ val ]);
}
return acc;
}, []) }
isOpen = { overflowMenuVisible }
key = 'overflow-menu'
onToolboxEscKey = { onEscKey }
onVisibilityChange = { onSetOverflowVisible }
showRaiseHandInReactionsMenu = { showRaiseHandInReactionsMenu }
showReactionsMenu = { showReactionsInOverflowMenu } />
)}
{isButtonEnabled('hangup', toolbarButtonsToUse) && (
endConferenceSupported
? <HangupMenuButton
ariaControls = 'hangup-menu'
isOpen = { hangupMenuVisible }
key = 'hangup-menu'
notifyMode = { buttonsWithNotifyClick?.get('hangup-menu') }
onVisibilityChange = { onSetHangupVisible }>
<ContextMenu
accessibilityLabel = { t(toolbarAccLabel) }
className = { classes.hangupMenu }
hidden = { false }
inDrawer = { overflowDrawer }
onKeyDown = { onEscKey }>
<EndConferenceButton
buttonKey = 'end-meeting'
notifyMode = { buttonsWithNotifyClick?.get('end-meeting') } />
<LeaveConferenceButton
buttonKey = 'hangup'
notifyMode = { buttonsWithNotifyClick?.get('hangup') } />
</ContextMenu>
</HangupMenuButton>
: <HangupButton
buttonKey = 'hangup'
customClass = 'hangup-button'
key = 'hangup-button'
notifyMode = { buttonsWithNotifyClick.get('hangup') }
visible = { isButtonEnabled('hangup', toolbarButtonsToUse) } />
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,203 @@
import React, { ReactElement } from 'react';
import { connect } from 'react-redux';
import { withStyles } from 'tss-react/mui';
import { ACTION_SHORTCUT_TRIGGERED, VIDEO_MUTE, createShortcutEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IGUMPendingState } from '../../../base/media/types';
import AbstractButton from '../../../base/toolbox/components/AbstractButton';
import Spinner from '../../../base/ui/components/web/Spinner';
import { registerShortcut, unregisterShortcut } from '../../../keyboard-shortcuts/actions';
import { SPINNER_COLOR } from '../../constants';
import AbstractVideoMuteButton, {
IProps as AbstractVideoMuteButtonProps,
mapStateToProps as abstractMapStateToProps
} from '../AbstractVideoMuteButton';
const styles = () => {
return {
pendingContainer: {
position: 'absolute' as const,
bottom: '3px',
right: '3px'
}
};
};
/**
* The type of the React {@code Component} props of {@link VideoMuteButton}.
*/
export interface IProps extends AbstractVideoMuteButtonProps {
/**
* The gumPending state from redux.
*/
_gumPending: IGUMPendingState;
/**
* An object containing the CSS classes.
*/
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
}
/**
* Component that renders a toolbar button for toggling video mute.
*
* @augments AbstractVideoMuteButton
*/
class VideoMuteButton extends AbstractVideoMuteButton<IProps> {
/**
* Initializes a new {@code VideoMuteButton} instance.
*
* @param {IProps} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props: IProps) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onKeyboardShortcut = this._onKeyboardShortcut.bind(this);
this._getTooltip = this._getLabel;
}
/**
* Registers the keyboard shortcut that toggles the video muting.
*
* @inheritdoc
* @returns {void}
*/
override componentDidMount() {
this.props.dispatch(registerShortcut({
character: 'V',
helpDescription: 'keyboardShortcuts.videoMute',
handler: this._onKeyboardShortcut
}));
}
/**
* Unregisters the keyboard shortcut that toggles the video muting.
*
* @inheritdoc
* @returns {void}
*/
override componentWillUnmount() {
this.props.dispatch(unregisterShortcut('V'));
}
/**
* Gets the current accessibility label, taking the toggled and GUM pending state into account. If no toggled label
* is provided, the regular accessibility label will also be used in the toggled state.
*
* The accessibility label is not visible in the UI, it is meant to be used by assistive technologies, mainly screen
* readers.
*
* @private
* @returns {string}
*/
override _getAccessibilityLabel() {
const { _gumPending } = this.props;
if (_gumPending === IGUMPendingState.NONE) {
return super._getAccessibilityLabel();
}
return 'toolbar.accessibilityLabel.videomuteGUMPending';
}
/**
* Gets the current label, taking the toggled and GUM pending state into account. If no
* toggled label is provided, the regular label will also be used in the toggled state.
*
* @private
* @returns {string}
*/
override _getLabel() {
const { _gumPending } = this.props;
if (_gumPending === IGUMPendingState.NONE) {
return super._getLabel();
}
return 'toolbar.videomuteGUMPending';
}
/**
* Indicates if video is currently muted or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isVideoMuted() {
if (this.props._gumPending === IGUMPendingState.PENDING_UNMUTE) {
return false;
}
return super._isVideoMuted();
}
/**
* Returns a spinner if there is pending GUM.
*
* @returns {ReactElement | null}
*/
override _getElementAfter(): ReactElement | null {
const { _gumPending } = this.props;
const classes = withStyles.getClasses(this.props);
return _gumPending === IGUMPendingState.NONE ? null
: (
<div className = { classes.pendingContainer }>
<Spinner
color = { SPINNER_COLOR }
size = 'small' />
</div>
);
}
/**
* Creates an analytics keyboard shortcut event and dispatches an action to
* toggle the video muting.
*
* @private
* @returns {void}
*/
_onKeyboardShortcut() {
// Ignore keyboard shortcuts if the video button is disabled.
if (this._isDisabled()) {
return;
}
sendAnalytics(
createShortcutEvent(
VIDEO_MUTE,
ACTION_SHORTCUT_TRIGGERED,
{ enable: !this._isVideoMuted() }));
AbstractButton.prototype._onClick.call(this);
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code VideoMuteButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _videoMuted: boolean
* }}
*/
function _mapStateToProps(state: IReduxState) {
const { gumPending } = state['features/base/media'].video;
return {
...abstractMapStateToProps(state),
_gumPending: gumPending
};
}
export default withStyles(translate(connect(_mapStateToProps)(VideoMuteButton)), styles);

View File

@@ -0,0 +1,197 @@
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n/functions';
import { IconArrowUp } from '../../../base/icons/svg';
import { IGUMPendingState } from '../../../base/media/types';
import ToolboxButtonWithIcon from '../../../base/toolbox/components/web/ToolboxButtonWithIcon';
import { getLocalJitsiVideoTrack } from '../../../base/tracks/functions.web';
import { toggleVideoSettings } from '../../../settings/actions';
import VideoSettingsPopup from '../../../settings/components/web/video/VideoSettingsPopup';
import { getVideoSettingsVisibility } from '../../../settings/functions.web';
import { isVideoSettingsButtonDisabled } from '../../functions.web';
import VideoMuteButton from './VideoMuteButton';
interface IProps extends WithTranslation {
/**
* The button's key.
*/
buttonKey?: string;
/**
* The gumPending state from redux.
*/
gumPending: IGUMPendingState;
/**
* External handler for click action.
*/
handleClick: Function;
/**
* Indicates whether video permissions have been granted or denied.
*/
hasPermissions: boolean;
/**
* Whether there is a video track or not.
*/
hasVideoTrack: boolean;
/**
* If the button should be disabled.
*/
isDisabled: boolean;
/**
* Defines is popup is open.
*/
isOpen: boolean;
/**
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
*/
notifyMode?: string;
/**
* Click handler for the small icon. Opens video options.
*/
onVideoOptionsClick: Function;
/**
* Flag controlling the visibility of the button.
* VideoSettings popup is currently disabled on mobile browsers
* as mobile devices do not support capture of more than one
* camera at a time.
*/
visible: boolean;
}
/**
* Button used for video & video settings.
*
* @returns {ReactElement}
*/
class VideoSettingsButton extends Component<IProps> {
/**
* Initializes a new {@code VideoSettingsButton} instance.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._onEscClick = this._onEscClick.bind(this);
this._onClick = this._onClick.bind(this);
}
/**
* Returns true if the settings icon is disabled.
*
* @returns {boolean}
*/
_isIconDisabled() {
const { gumPending, hasPermissions, hasVideoTrack, isDisabled } = this.props;
return ((!hasPermissions || isDisabled) && !hasVideoTrack) || gumPending !== IGUMPendingState.NONE;
}
/**
* Click handler for the more actions entries.
*
* @param {KeyboardEvent} event - Esc key click to close the popup.
* @returns {void}
*/
_onEscClick(event: React.KeyboardEvent) {
if (event.key === 'Escape' && this.props.isOpen) {
event.preventDefault();
event.stopPropagation();
this._onClick();
}
}
/**
* Click handler for the more actions entries.
*
* @param {MouseEvent} e - Mousw event.
* @returns {void}
*/
_onClick(e?: React.MouseEvent) {
const { onVideoOptionsClick, isOpen } = this.props;
if (isOpen) {
e?.stopPropagation();
}
onVideoOptionsClick();
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
override render() {
const { gumPending, t, visible, isOpen, buttonKey, notifyMode } = this.props;
return visible ? (
<VideoSettingsPopup>
<ToolboxButtonWithIcon
ariaControls = 'video-settings-dialog'
ariaExpanded = { isOpen }
ariaHasPopup = { true }
ariaLabel = { this.props.t('toolbar.videoSettings') }
buttonKey = { buttonKey }
icon = { IconArrowUp }
iconDisabled = { this._isIconDisabled() || gumPending !== IGUMPendingState.NONE }
iconId = 'video-settings-button'
iconTooltip = { t('toolbar.videoSettings') }
notifyMode = { notifyMode }
onIconClick = { this._onClick }
onIconKeyDown = { this._onEscClick }>
<VideoMuteButton
buttonKey = { buttonKey }
notifyMode = { notifyMode } />
</ToolboxButtonWithIcon>
</VideoSettingsPopup>
) : <VideoMuteButton
buttonKey = { buttonKey }
notifyMode = { notifyMode } />;
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
function mapStateToProps(state: IReduxState) {
const { permissions = { video: false } } = state['features/base/devices'];
const { isNarrowLayout } = state['features/base/responsive-ui'];
const { gumPending } = state['features/base/media'].video;
return {
gumPending,
hasPermissions: permissions.video,
hasVideoTrack: Boolean(getLocalJitsiVideoTrack(state)),
isDisabled: isVideoSettingsButtonDisabled(state),
isOpen: Boolean(getVideoSettingsVisibility(state)),
visible: !isMobileBrowser() && !isNarrowLayout
};
}
const mapDispatchToProps = {
onVideoOptionsClick: toggleVideoSettings
};
export default translate(connect(
mapStateToProps,
mapDispatchToProps
)(VideoSettingsButton));