extends AbstractMuteRemoteParticipantsVideoDialog {
+ static defaultProps = {
+ exclude: [],
+ muteLocal: false
+ };
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const { content, title } = this.props;
+
+ return (
+
+ );
+ }
+
+ _onSubmit: () => boolean;
+
+ /**
+ * Callback to be invoked when the value of this dialog is submitted.
+ *
+ * @returns {boolean}
+ */
+ _onSubmit() {
+ const {
+ dispatch,
+ exclude
+ } = this.props;
+
+ dispatch(muteAllParticipants(exclude, MEDIA_TYPE.VIDEO));
+
+ return true;
+ }
+}
+
+/**
+ * Maps (parts of) the Redux state to the associated {@code AbstractMuteEveryonesVideoDialog}'s props.
+ *
+ * @param {Object} state - The redux state.
+ * @param {Object} ownProps - The properties explicitly passed to the component.
+ * @returns {Props}
+ */
+export function abstractMapStateToProps(state: Object, ownProps: Props) {
+ const { exclude, t } = ownProps;
+
+ const whom = exclude
+ // eslint-disable-next-line no-confusing-arrow
+ .map(id => id === getLocalParticipant(state).id
+ ? t('dialog.muteEveryoneSelf')
+ : getParticipantDisplayName(state, id))
+ .join(', ');
+
+ return whom.length ? {
+ content: t('dialog.muteEveryoneElsesVideoDialog'),
+ title: t('dialog.muteEveryoneElsesVideoTitle', { whom })
+ } : {
+ content: t('dialog.muteEveryonesVideoDialog'),
+ title: t('dialog.muteEveryonesVideoTitle')
+ };
+}
diff --git a/react/features/remote-video-menu/components/AbstractMuteRemoteParticipantDialog.js b/react/features/remote-video-menu/components/AbstractMuteRemoteParticipantDialog.js
index b4e01685f7..b7322a2227 100644
--- a/react/features/remote-video-menu/components/AbstractMuteRemoteParticipantDialog.js
+++ b/react/features/remote-video-menu/components/AbstractMuteRemoteParticipantDialog.js
@@ -2,6 +2,7 @@
import { Component } from 'react';
+import { MEDIA_TYPE } from '../../base/media';
import { muteRemote } from '../actions';
/**
@@ -57,7 +58,7 @@ export default class AbstractMuteRemoteParticipantDialog
_onSubmit() {
const { dispatch, participantID } = this.props;
- dispatch(muteRemote(participantID));
+ dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO));
return true;
}
diff --git a/react/features/remote-video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js b/react/features/remote-video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js
new file mode 100644
index 0000000000..d32db1375f
--- /dev/null
+++ b/react/features/remote-video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js
@@ -0,0 +1,65 @@
+// @flow
+
+import { Component } from 'react';
+
+import { MEDIA_TYPE } from '../../base/media';
+import { muteRemote } from '../actions';
+
+/**
+ * The type of the React {@code Component} props of
+ * {@link AbstractMuteRemoteParticipantsVideoDialog}.
+ */
+export type Props = {
+
+ /**
+ * The Redux dispatch function.
+ */
+ dispatch: Function,
+
+ /**
+ * The ID of the remote participant to be muted.
+ */
+ participantID: string,
+
+ /**
+ * Function to translate i18n labels.
+ */
+ t: Function
+};
+
+/**
+ * Abstract dialog to confirm a remote participant video ute action.
+ *
+ * @extends Component
+ */
+export default class AbstractMuteRemoteParticipantsVideoDialog
+ extends Component {
+ /**
+ * Initializes a new {@code AbstractMuteRemoteParticipantsVideoDialog} instance.
+ *
+ * @param {Object} props - The read-only properties with which the new
+ * instance is to be initialized.
+ */
+ constructor(props: P) {
+ super(props);
+
+ // Bind event handlers so they are only bound once per instance.
+ this._onSubmit = this._onSubmit.bind(this);
+ }
+
+ _onSubmit: () => boolean;
+
+ /**
+ * Handles the submit button action.
+ *
+ * @private
+ * @returns {boolean} - True (to note that the modal should be closed).
+ */
+ _onSubmit() {
+ const { dispatch, participantID } = this.props;
+
+ dispatch(muteRemote(participantID, MEDIA_TYPE.VIDEO));
+
+ return true;
+ }
+}
diff --git a/react/features/remote-video-menu/components/AbstractMuteVideoButton.js b/react/features/remote-video-menu/components/AbstractMuteVideoButton.js
new file mode 100644
index 0000000000..1b72a23834
--- /dev/null
+++ b/react/features/remote-video-menu/components/AbstractMuteVideoButton.js
@@ -0,0 +1,103 @@
+// @flow
+
+import {
+ createRemoteVideoMenuButtonEvent,
+ sendAnalytics
+} from '../../analytics';
+import { openDialog } from '../../base/dialog';
+import { IconCameraDisabled } from '../../base/icons';
+import { MEDIA_TYPE } from '../../base/media';
+import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
+import { isRemoteTrackMuted } from '../../base/tracks';
+
+import { MuteRemoteParticipantsVideoDialog } from '.';
+
+export type Props = AbstractButtonProps & {
+
+ /**
+ * Boolean to indicate if the video track of the participant is muted or
+ * not.
+ */
+ _videoTrackMuted: boolean,
+
+ /**
+ * The redux {@code dispatch} function.
+ */
+ dispatch: Function,
+
+ /**
+ * The ID of the participant object that this button is supposed to
+ * mute/unmute.
+ */
+ participantID: string,
+
+ /**
+ * The function to be used to translate i18n labels.
+ */
+ t: Function
+};
+
+/**
+ * An abstract remote video menu button which mutes the remote participant.
+ */
+export default class AbstractMuteVideoButton extends AbstractButton {
+ accessibilityLabel = 'toolbar.accessibilityLabel.remoteVideoMute';
+ icon = IconCameraDisabled;
+ label = 'videothumbnail.domuteVideo';
+ toggledLabel = 'videothumbnail.videoMuted';
+
+ /**
+ * Handles clicking / pressing the button, and mutes the participant.
+ *
+ * @private
+ * @returns {void}
+ */
+ _handleClick() {
+ const { dispatch, participantID } = this.props;
+
+ sendAnalytics(createRemoteVideoMenuButtonEvent(
+ 'mute.button',
+ {
+ 'participant_id': participantID
+ }));
+
+ dispatch(openDialog(MuteRemoteParticipantsVideoDialog, { participantID }));
+ }
+
+ /**
+ * Renders the item disabled if the participant is muted.
+ *
+ * @inheritdoc
+ */
+ _isDisabled() {
+ return this.props._videoTrackMuted;
+ }
+
+ /**
+ * Renders the item toggled if the participant is muted.
+ *
+ * @inheritdoc
+ */
+ _isToggled() {
+ return this.props._videoTrackMuted;
+ }
+}
+
+/**
+ * Function that maps parts of Redux state tree into component props.
+ *
+ * @param {Object} state - Redux state.
+ * @param {Object} ownProps - Properties of component.
+ * @private
+ * @returns {{
+ * _videoTrackMuted: boolean
+ * }}
+ */
+export function _mapStateToProps(state: Object, ownProps: Props) {
+ const tracks = state['features/base/tracks'];
+
+ return {
+ _videoTrackMuted: isRemoteTrackMuted(
+ tracks, MEDIA_TYPE.VIDEO, ownProps.participantID)
+ };
+}
diff --git a/react/features/remote-video-menu/components/web/MuteEveryoneElsesVideoButton.js b/react/features/remote-video-menu/components/web/MuteEveryoneElsesVideoButton.js
new file mode 100644
index 0000000000..99e4eb1c2c
--- /dev/null
+++ b/react/features/remote-video-menu/components/web/MuteEveryoneElsesVideoButton.js
@@ -0,0 +1,54 @@
+// @flow
+
+import React from 'react';
+
+import { translate } from '../../../base/i18n';
+import { IconMuteVideoEveryoneElse } from '../../../base/icons';
+import { connect } from '../../../base/redux';
+import AbstractMuteEveryoneElsesVideoButton, {
+ type Props
+} from '../AbstractMuteEveryoneElsesVideoButton';
+
+import RemoteVideoMenuButton from './RemoteVideoMenuButton';
+
+/**
+ * Implements a React {@link Component} which displays a button for audio muting
+ * every participant in the conference except the one with the given
+ * participantID
+ */
+class MuteEveryoneElsesVideoButton extends AbstractMuteEveryoneElsesVideoButton {
+ /**
+ * Instantiates a new {@code Component}.
+ *
+ * @inheritdoc
+ */
+ constructor(props: Props) {
+ super(props);
+
+ this._handleClick = this._handleClick.bind(this);
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const { participantID, t } = this.props;
+
+ return (
+
+ );
+ }
+
+ _handleClick: () => void;
+}
+
+export default translate(connect()(MuteEveryoneElsesVideoButton));
diff --git a/react/features/remote-video-menu/components/web/MuteEveryonesVideoDialog.js b/react/features/remote-video-menu/components/web/MuteEveryonesVideoDialog.js
new file mode 100644
index 0000000000..592c595407
--- /dev/null
+++ b/react/features/remote-video-menu/components/web/MuteEveryonesVideoDialog.js
@@ -0,0 +1,41 @@
+// @flow
+
+import React from 'react';
+
+import { Dialog } from '../../../base/dialog';
+import { translate } from '../../../base/i18n';
+import { connect } from '../../../base/redux';
+import AbstractMuteEveryonesVideoDialog, { abstractMapStateToProps, type Props }
+ from '../AbstractMuteEveryonesVideoDialog';
+
+/**
+ * A React Component with the contents for a dialog that asks for confirmation
+ * from the user before disabling all remote participants cameras.
+ *
+ * @extends AbstractMuteEveryonesVideoDialog
+ */
+class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog {
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ return (
+
+ );
+ }
+
+ _onSubmit: () => boolean;
+}
+
+export default translate(connect(abstractMapStateToProps)(MuteEveryonesVideoDialog));
diff --git a/react/features/remote-video-menu/components/web/MuteRemoteParticipantsVideoDialog.js b/react/features/remote-video-menu/components/web/MuteRemoteParticipantsVideoDialog.js
new file mode 100644
index 0000000000..292e456552
--- /dev/null
+++ b/react/features/remote-video-menu/components/web/MuteRemoteParticipantsVideoDialog.js
@@ -0,0 +1,41 @@
+/* @flow */
+
+import React from 'react';
+
+import { Dialog } from '../../../base/dialog';
+import { translate } from '../../../base/i18n';
+import { connect } from '../../../base/redux';
+import AbstractMuteRemoteParticipantsVideoDialog
+ from '../AbstractMuteRemoteParticipantsVideoDialog';
+
+/**
+ * A React Component with the contents for a dialog that asks for confirmation
+ * from the user before disabling a remote participants camera.
+ *
+ * @extends Component
+ */
+class MuteRemoteParticipantsVideoDialog extends AbstractMuteRemoteParticipantsVideoDialog {
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ return (
+
+ );
+ }
+
+ _onSubmit: () => boolean;
+}
+
+export default translate(connect()(MuteRemoteParticipantsVideoDialog));
diff --git a/react/features/remote-video-menu/components/web/MuteVideoButton.js b/react/features/remote-video-menu/components/web/MuteVideoButton.js
new file mode 100644
index 0000000000..f6863f5ebb
--- /dev/null
+++ b/react/features/remote-video-menu/components/web/MuteVideoButton.js
@@ -0,0 +1,67 @@
+/* @flow */
+
+import React from 'react';
+
+import { translate } from '../../../base/i18n';
+import { IconCameraDisabled } from '../../../base/icons';
+import { connect } from '../../../base/redux';
+import AbstractMuteVideoButton, {
+ _mapStateToProps,
+ type Props
+} from '../AbstractMuteVideoButton';
+
+import RemoteVideoMenuButton from './RemoteVideoMenuButton';
+
+/**
+ * Implements a React {@link Component} which displays a button for disabling
+ * the camera of a participant in the conference.
+ *
+ * NOTE: At the time of writing this is a button that doesn't use the
+ * {@code AbstractButton} base component, but is inherited from the same
+ * super class ({@code AbstractMuteVideoButton} that extends {@code AbstractButton})
+ * for the sake of code sharing between web and mobile. Once web uses the
+ * {@code AbstractButton} base component, this can be fully removed.
+ */
+class MuteVideoButton extends AbstractMuteVideoButton {
+ /**
+ * Instantiates a new {@code Component}.
+ *
+ * @inheritdoc
+ */
+ constructor(props: Props) {
+ super(props);
+
+ this._handleClick = this._handleClick.bind(this);
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const { _videoTrackMuted, participantID, t } = this.props;
+ const muteConfig = _videoTrackMuted ? {
+ translationKey: 'videothumbnail.videoMuted',
+ muteClassName: 'mutelink disabled'
+ } : {
+ translationKey: 'videothumbnail.domuteVideo',
+ muteClassName: 'mutelink'
+ };
+
+ return (
+
+ );
+ }
+
+ _handleClick: () => void
+}
+
+export default translate(connect(_mapStateToProps)(MuteVideoButton));
diff --git a/react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js b/react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js
index 4d1e9e3f5a..9d6c9866dd 100644
--- a/react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js
+++ b/react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js
@@ -3,20 +3,20 @@
import React, { Component } from 'react';
import { Icon, IconMenuThumb } from '../../../base/icons';
-import { MEDIA_TYPE } from '../../../base/media';
import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants';
import { Popover } from '../../../base/popover';
import { connect } from '../../../base/redux';
-import { isRemoteTrackMuted } from '../../../base/tracks';
import { requestRemoteControl, stopController } from '../../../remote-control';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
+import MuteEveryoneElsesVideoButton from './MuteEveryoneElsesVideoButton';
import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
import {
GrantModeratorButton,
MuteButton,
+ MuteVideoButton,
KickButton,
PrivateMessageMenuButton,
RemoteControlButton,
@@ -43,11 +43,6 @@ type Props = {
*/
_disableRemoteMute: Boolean,
- /**
- * Whether or not the participant is currently muted.
- */
- _isAudioMuted: boolean,
-
/**
* Whether or not the participant is a conference moderator.
*/
@@ -151,7 +146,6 @@ class RemoteVideoMenuTriggerButton extends Component {
const {
_disableKick,
_disableRemoteMute,
- _isAudioMuted,
_isModerator,
dispatch,
initialVolumeValue,
@@ -166,7 +160,6 @@ class RemoteVideoMenuTriggerButton extends Component {
if (!_disableRemoteMute) {
buttons.push(
);
@@ -175,6 +168,16 @@ class RemoteVideoMenuTriggerButton extends Component {
key = 'mute-others'
participantID = { participantID } />
);
+ buttons.push(
+
+ );
+ buttons.push(
+
+ );
}
buttons.push(
@@ -247,7 +250,6 @@ class RemoteVideoMenuTriggerButton extends Component {
*/
function _mapStateToProps(state, ownProps) {
const { participantID } = ownProps;
- const tracks = state['features/base/tracks'];
const localParticipant = getLocalParticipant(state);
const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
const { disableKick } = remoteVideoMenu;
@@ -286,7 +288,6 @@ function _mapStateToProps(state, ownProps) {
}
return {
- _isAudioMuted: isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID) || false,
_isModerator: Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR),
_disableKick: Boolean(disableKick),
_disableRemoteMute: Boolean(disableRemoteMute),
diff --git a/react/features/remote-video-menu/components/web/index.js b/react/features/remote-video-menu/components/web/index.js
index 78cd6b8ddc..7a80e58e4b 100644
--- a/react/features/remote-video-menu/components/web/index.js
+++ b/react/features/remote-video-menu/components/web/index.js
@@ -5,9 +5,13 @@ export { default as GrantModeratorDialog } from './GrantModeratorDialog';
export { default as KickButton } from './KickButton';
export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog';
export { default as MuteButton } from './MuteButton';
+export { default as MuteVideoButton } from './MuteVideoButton';
export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
+export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog';
export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton';
+export { default as MuteEveryoneElsesVideoButton } from './MuteEveryoneElsesVideoButton';
export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
+export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
export { default as RemoteVideoMenu } from './RemoteVideoMenu';
diff --git a/react/features/toolbox/components/AudioMuteButton.js b/react/features/toolbox/components/AudioMuteButton.js
index 9599ed1e67..c03bf641bf 100644
--- a/react/features/toolbox/components/AudioMuteButton.js
+++ b/react/features/toolbox/components/AudioMuteButton.js
@@ -125,7 +125,7 @@ class AudioMuteButton extends AbstractAudioMuteButton {
* @returns {void}
*/
_setAudioMuted(audioMuted: boolean) {
- this.props.dispatch(muteLocal(audioMuted));
+ this.props.dispatch(muteLocal(audioMuted, MEDIA_TYPE.AUDIO));
}
/**
diff --git a/react/features/toolbox/components/MuteEveryonesVideoButton.js b/react/features/toolbox/components/MuteEveryonesVideoButton.js
new file mode 100644
index 0000000000..750485bce1
--- /dev/null
+++ b/react/features/toolbox/components/MuteEveryonesVideoButton.js
@@ -0,0 +1,76 @@
+// @flow
+
+import { createToolbarEvent, sendAnalytics } from '../../analytics';
+import { openDialog } from '../../base/dialog';
+import { translate } from '../../base/i18n';
+import { IconMuteVideoEveryone } from '../../base/icons';
+import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
+import { connect } from '../../base/redux';
+import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
+import { MuteEveryonesVideoDialog } from '../../remote-video-menu/components';
+
+type Props = AbstractButtonProps & {
+
+ /**
+ * The Redux dispatch function.
+ */
+ dispatch: Function,
+
+ /*
+ ** Whether the local participant is a moderator or not.
+ */
+ isModerator: Boolean,
+
+ /**
+ * The ID of the local participant.
+ */
+ localParticipantId: string
+};
+
+/**
+ * Implements a React {@link Component} which displays a button for disabling the camera of
+ * every participant (except the local one)
+ */
+class MuteEveryonesVideoButton extends AbstractButton {
+ accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryonesVideo';
+ icon = IconMuteVideoEveryone;
+ label = 'toolbar.muteEveryonesVideo';
+ tooltip = 'toolbar.muteVideoEveryone';
+
+ /**
+ * Handles clicking / pressing the button, and opens a confirmation dialog.
+ *
+ * @private
+ * @returns {void}
+ */
+ _handleClick() {
+ const { dispatch, localParticipantId } = this.props;
+
+ sendAnalytics(createToolbarEvent('mute.everyone.pressed'));
+ dispatch(openDialog(MuteEveryonesVideoDialog, {
+ exclude: [ localParticipantId ]
+ }));
+ }
+}
+
+/**
+ * Maps part of the redux state to the component's props.
+ *
+ * @param {Object} state - The redux store/state.
+ * @param {Props} ownProps - The component's own props.
+ * @returns {Object}
+ */
+function _mapStateToProps(state: Object, ownProps: Props) {
+ const localParticipant = getLocalParticipant(state);
+ const isModerator = localParticipant.role === PARTICIPANT_ROLE.MODERATOR;
+ const { visible } = ownProps;
+ const { disableRemoteMute } = state['features/base/config'];
+
+ return {
+ isModerator,
+ localParticipantId: localParticipant.id,
+ visible: visible && isModerator && !disableRemoteMute
+ };
+}
+
+export default translate(connect(_mapStateToProps)(MuteEveryonesVideoButton));
diff --git a/react/features/toolbox/components/web/Toolbox.js b/react/features/toolbox/components/web/Toolbox.js
index a8c9bbaa6c..9cd2eb7dbe 100644
--- a/react/features/toolbox/components/web/Toolbox.js
+++ b/react/features/toolbox/components/web/Toolbox.js
@@ -82,6 +82,7 @@ import DownloadButton from '../DownloadButton';
import HangupButton from '../HangupButton';
import HelpButton from '../HelpButton';
import MuteEveryoneButton from '../MuteEveryoneButton';
+import MuteEveryonesVideoButton from '../MuteEveryonesVideoButton';
import AudioSettingsButton from './AudioSettingsButton';
import OverflowMenuButton from './OverflowMenuButton';
@@ -1079,6 +1080,10 @@ class Toolbox extends Component {
&& ,
+ this._shouldShowButton('mute-video-everyone')
+ && ,
this._shouldShowButton('stats')
&&