This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
59
react/features/toolbox/components/DownloadButton.ts
Normal file
59
react/features/toolbox/components/DownloadButton.ts
Normal 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));
|
||||
50
react/features/toolbox/components/HangupButton.ts
Normal file
50
react/features/toolbox/components/HangupButton.ts
Normal 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));
|
||||
62
react/features/toolbox/components/HelpButton.ts
Normal file
62
react/features/toolbox/components/HelpButton.ts
Normal 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));
|
||||
1
react/features/toolbox/components/index.native.ts
Normal file
1
react/features/toolbox/components/index.native.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as CustomOptionButton } from './native/CustomOptionButton';
|
||||
1
react/features/toolbox/components/index.web.ts
Normal file
1
react/features/toolbox/components/index.web.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as CustomOptionButton } from './web/CustomOptionButton';
|
||||
@@ -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>));
|
||||
96
react/features/toolbox/components/native/AudioOnlyButton.ts
Normal file
96
react/features/toolbox/components/native/AudioOnlyButton.ts
Normal 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));
|
||||
@@ -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));
|
||||
@@ -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;
|
||||
77
react/features/toolbox/components/native/HangupMenu.tsx
Normal file
77
react/features/toolbox/components/native/HangupMenu.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
316
react/features/toolbox/components/native/OverflowMenu.tsx
Normal file
316
react/features/toolbox/components/native/OverflowMenu.tsx
Normal 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 } />
|
||||
);
|
||||
});
|
||||
@@ -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));
|
||||
99
react/features/toolbox/components/native/RaiseHandButton.ts
Normal file
99
react/features/toolbox/components/native/RaiseHandButton.ts
Normal 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));
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
142
react/features/toolbox/components/native/Toolbox.tsx
Normal file
142
react/features/toolbox/components/native/Toolbox.tsx
Normal 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);
|
||||
@@ -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>));
|
||||
216
react/features/toolbox/components/native/styles.ts
Normal file
216
react/features/toolbox/components/native/styles.ts
Normal 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'
|
||||
}
|
||||
});
|
||||
205
react/features/toolbox/components/web/AudioMuteButton.tsx
Normal file
205
react/features/toolbox/components/web/AudioMuteButton.tsx
Normal 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);
|
||||
180
react/features/toolbox/components/web/AudioSettingsButton.tsx
Normal file
180
react/features/toolbox/components/web/AudioSettingsButton.tsx
Normal 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));
|
||||
40
react/features/toolbox/components/web/CustomOptionButton.tsx
Normal file
40
react/features/toolbox/components/web/CustomOptionButton.tsx
Normal 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;
|
||||
126
react/features/toolbox/components/web/DialogPortal.ts
Normal file
126
react/features/toolbox/components/web/DialogPortal.ts
Normal 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;
|
||||
172
react/features/toolbox/components/web/Drawer.tsx
Normal file
172
react/features/toolbox/components/web/Drawer.tsx
Normal 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;
|
||||
@@ -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 } /> }
|
||||
</>);
|
||||
};
|
||||
77
react/features/toolbox/components/web/FullscreenButton.ts
Normal file
77
react/features/toolbox/components/web/FullscreenButton.ts
Normal 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));
|
||||
@@ -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 } />
|
||||
);
|
||||
};
|
||||
|
||||
134
react/features/toolbox/components/web/HangupMenuButton.tsx
Normal file
134
react/features/toolbox/components/web/HangupMenuButton.tsx
Normal 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);
|
||||
57
react/features/toolbox/components/web/HangupToggleButton.tsx
Normal file
57
react/features/toolbox/components/web/HangupToggleButton.tsx
Normal 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));
|
||||
58
react/features/toolbox/components/web/JitsiPortal.tsx
Normal file
58
react/features/toolbox/components/web/JitsiPortal.tsx
Normal 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;
|
||||
@@ -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 } />
|
||||
);
|
||||
};
|
||||
@@ -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));
|
||||
262
react/features/toolbox/components/web/OverflowMenuButton.tsx
Normal file
262
react/features/toolbox/components/web/OverflowMenuButton.tsx
Normal 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;
|
||||
@@ -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));
|
||||
116
react/features/toolbox/components/web/ProfileButton.ts
Normal file
116
react/features/toolbox/components/web/ProfileButton.ts
Normal 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));
|
||||
@@ -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);
|
||||
3
react/features/toolbox/components/web/Separator.tsx
Normal file
3
react/features/toolbox/components/web/Separator.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
export default () => <hr className = 'overflow-menu-hr' />;
|
||||
116
react/features/toolbox/components/web/ShareDesktopButton.ts
Normal file
116
react/features/toolbox/components/web/ShareDesktopButton.ts
Normal 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));
|
||||
76
react/features/toolbox/components/web/ToggleCameraButton.ts
Normal file
76
react/features/toolbox/components/web/ToggleCameraButton.ts
Normal 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));
|
||||
334
react/features/toolbox/components/web/Toolbox.tsx
Normal file
334
react/features/toolbox/components/web/Toolbox.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
203
react/features/toolbox/components/web/VideoMuteButton.tsx
Normal file
203
react/features/toolbox/components/web/VideoMuteButton.tsx
Normal 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);
|
||||
197
react/features/toolbox/components/web/VideoSettingsButton.tsx
Normal file
197
react/features/toolbox/components/web/VideoSettingsButton.tsx
Normal 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));
|
||||
Reference in New Issue
Block a user