This commit is contained in:
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