feat(mute): mute everyone / everyone else

This commit is contained in:
Gabriel Imre 2020-02-24 14:47:37 +02:00 committed by Saúl Ibarra Corretgé
parent d7ece58c6f
commit 24a1a60f04
11 changed files with 369 additions and 23 deletions

View File

@ -51,7 +51,7 @@ var interfaceConfig = {
'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
'tileview', 'videobackgroundblur', 'download', 'help'
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone'
],
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],

View File

@ -209,6 +209,12 @@
"micNotSendingDataTitle": "Your mic is muted by your system settings",
"micPermissionDeniedError": "You have not granted permission to use your microphone. You can still join the conference but others won't hear you. Use the camera button in the address bar to fix this.",
"micUnknownError": "Cannot use microphone for an unknown reason.",
"muteEveryoneElseDialog": "Once muted, you won't be able to unmute them, but they can unmute themselves at any time.",
"muteEveryoneElseTitle": "Mute everyone except {{whom}}?",
"muteEveryoneDialog": "Are you sure you want to mute everyone? You won't be able to unmute them, but they can unmute themselves at any time.",
"muteEveryoneTitle": "Mute everyone?",
"muteEveryoneSelf": "yourself",
"muteEveryoneStartMuted": "Everyone starts muted from now on",
"muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
"muteParticipantButton": "Mute",
"muteParticipantDialog": "Are you sure you want to mute this participant? You won't be able to unmute them, but they can unmute themselves at any time.",
@ -592,6 +598,7 @@
"moreActionsMenu": "More actions menu",
"moreOptions": "Show more options",
"mute": "Toggle mute audio",
"muteEveryone": "Mute everyone",
"pip": "Toggle Picture-in-Picture mode",
"privateMessage": "Send private message",
"profile": "Edit your profile",
@ -635,6 +642,7 @@
"moreActions": "More actions",
"moreOptions": "More options",
"mute": "Mute / Unmute",
"muteEveryone": "Mute everyone",
"noAudioSignalTitle": "There is no input coming from your mic!",
"noAudioSignalDesc": "If you did not purposely mute it from system settings or hardware, consider switching the device.",
"noAudioSignalDescSuggestion": "If you did not purposely mute it from system settings or hardware, consider switching to the suggested device.",
@ -722,6 +730,7 @@
},
"videothumbnail": {
"domute": "Mute",
"domuteOthers": "Mute everyone else",
"flip": "Flip",
"kick": "Kick out",
"moderator": "Moderator",

View File

@ -1,9 +1,24 @@
// @flow
import type { Dispatch } from 'redux';
import {
AUDIO_MUTE,
createRemoteMuteConfirmedEvent,
createToolbarEvent,
sendAnalytics
} from '../analytics';
import { hideDialog } from '../base/dialog';
import {
getLocalParticipant,
muteRemoteParticipant
} from '../base/participants';
import { setAudioMuted } from '../base/media';
import UIEvents from '../../../service/UI/UIEvents';
import { RemoteVideoMenu } from './components';
declare var APP: Object;
/**
* Hides the remote video menu.
*
@ -12,3 +27,56 @@ import { RemoteVideoMenu } from './components';
export function hideRemoteVideoMenu() {
return hideDialog(RemoteVideoMenu);
}
/**
* Mutes the local participant.
*
* @param {boolean} enable - Whether to mute or unmute.
* @returns {Function}
*/
export function muteLocal(enable: boolean) {
return (dispatch: Dispatch<any>) => {
sendAnalytics(createToolbarEvent(AUDIO_MUTE, { enable }));
dispatch(setAudioMuted(enable, /* ensureTrack */ true));
// FIXME: The old conference logic as well as the shared video feature
// still rely on this event being emitted.
typeof APP === 'undefined'
|| APP.UI.emitEvent(UIEvents.AUDIO_MUTED, enable, true);
};
}
/**
* Mutes the remote participant with the given ID.
*
* @param {string} participantId - ID of the participant to mute.
* @returns {Function}
*/
export function muteRemote(participantId: string) {
return (dispatch: Dispatch<any>) => {
sendAnalytics(createRemoteMuteConfirmedEvent(participantId));
dispatch(muteRemoteParticipant(participantId));
};
}
/**
* Mutes all participants.
*
* @param {Array<string>} exclude - Array of participant IDs to not mute.
* @returns {Function}
*/
export function muteAllParticipants(exclude: Array<string>) {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const localId = getLocalParticipant(state).id;
const participantIds = state['features/base/participants']
.map(p => p.id);
/* eslint-disable no-confusing-arrow */
participantIds
.filter(id => !exclude.includes(id))
.map(id => id === localId ? muteLocal(true) : muteRemote(id))
.map(dispatch);
/* eslint-enable no-confusing-arrow */
};
}

View File

@ -2,17 +2,13 @@
import { Component } from 'react';
import {
createRemoteMuteConfirmedEvent,
sendAnalytics
} from '../../analytics';
import { muteRemoteParticipant } from '../../base/participants';
import { muteRemote } from '../actions';
/**
* The type of the React {@code Component} props of
* {@link AbstractMuteRemoteParticipantDialog}.
*/
type Props = {
export type Props = {
/**
* The Redux dispatch function.
@ -35,15 +31,15 @@ type Props = {
*
* @extends Component
*/
export default class AbstractMuteRemoteParticipantDialog
extends Component<Props> {
export default class AbstractMuteRemoteParticipantDialog<P:Props = Props>
extends Component<P> {
/**
* Initializes a new {@code AbstractMuteRemoteParticipantDialog} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
constructor(props: P) {
super(props);
// Bind event handlers so they are only bound once per instance.
@ -61,9 +57,7 @@ export default class AbstractMuteRemoteParticipantDialog
_onSubmit() {
const { dispatch, participantID } = this.props;
sendAnalytics(createRemoteMuteConfirmedEvent(participantID));
dispatch(muteRemoteParticipant(participantID));
dispatch(muteRemote(participantID));
return true;
}

View File

@ -0,0 +1,123 @@
// @flow
import React from 'react';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractMuteRemoteParticipantDialog, {
type Props as AbstractProps
} from '../AbstractMuteRemoteParticipantDialog';
import { muteAllParticipants } from '../../actions';
declare var APP: Object;
/**
* The type of the React {@code Component} props of
* {@link MuteEveryoneDialog}.
*/
type Props = AbstractProps & {
/**
* The IDs of the remote participants to exclude from being muted.
*/
exclude: Array<string>
};
/**
* Translations needed for dialog rendering.
*/
type Translations = {
/**
* Content text.
*/
content: string,
/**
* Title text.
*/
title: string
}
/**
* A React Component with the contents for a dialog that asks for confirmation
* from the user before muting a remote participant.
*
* @extends Component
*/
class MuteEveryoneDialog extends AbstractMuteRemoteParticipantDialog<Props> {
static defaultProps = {
exclude: [],
muteLocal: false
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { content, title } = this._getTranslations();
return (
<Dialog
okKey = 'dialog.muteParticipantButton'
onSubmit = { this._onSubmit }
titleString = { title }
width = 'small'>
<div>
{ content }
</div>
</Dialog>
);
}
_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));
return true;
}
/**
* Method to get translations depending on whether we have an exclusive
* mute or not.
*
* @returns {Translations}
* @private
*/
_getTranslations(): Translations {
const { exclude, t } = this.props;
const { conference } = APP;
const whom = exclude
// eslint-disable-next-line no-confusing-arrow
.map(id => conference.isLocalId(id)
? t('dialog.muteEveryoneSelf')
: conference.getParticipantDisplayName(id))
.join(', ');
return whom.length ? {
content: t('dialog.muteEveryoneElseDialog'),
title: t('dialog.muteEveryoneElseTitle', { whom })
} : {
content: t('dialog.muteEveryoneDialog'),
title: t('dialog.muteEveryoneTitle')
};
}
}
export default translate(connect()(MuteEveryoneDialog));

View File

@ -0,0 +1,71 @@
// @flow
import React from 'react';
import { createToolbarEvent, sendAnalytics } from '../../../analytics';
import { openDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { IconMicDisabled } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractMuteButton, {
_mapStateToProps,
type Props
} from '../AbstractMuteButton';
import MuteEveryoneDialog from './MuteEveryoneDialog';
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 MuteEveryoneElseButton extends AbstractMuteButton {
/**
* Instantiates a new {@code MuteEveryoneElseButton}.
*
* @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 (
<RemoteVideoMenuButton
buttonText = { t('videothumbnail.domuteOthers') }
displayClass = { 'mutelink' }
icon = { IconMicDisabled }
id = { `mutelink_${participantID}` }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick } />
);
}
_handleClick: () => void;
/**
* Handles clicking / pressing the button, and opens a confirmation dialog.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participantID } = this.props;
sendAnalytics(createToolbarEvent('mute.everyoneelse.pressed'));
dispatch(openDialog(MuteEveryoneDialog, { exclude: [ participantID ] }));
}
}
export default translate(connect(_mapStateToProps)(MuteEveryoneElseButton));

View File

@ -9,6 +9,7 @@ import { connect } from '../../../base/redux';
import {
MuteButton,
MuteEveryoneElseButton,
KickButton,
PrivateMessageMenuButton,
RemoteControlButton,
@ -174,6 +175,11 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
key = 'mute'
participantID = { participantID } />
);
buttons.push(
<MuteEveryoneElseButton
key = 'mute-others'
participantID = { participantID } />
);
buttons.push(
<KickButton
key = 'kick'

View File

@ -5,6 +5,8 @@ export {
default as KickRemoteParticipantDialog
} from './KickRemoteParticipantDialog';
export { default as MuteButton } from './MuteButton';
export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton';
export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
export {
default as MuteRemoteParticipantDialog
} from './MuteRemoteParticipantDialog';

View File

@ -4,16 +4,15 @@ import {
ACTION_SHORTCUT_TRIGGERED,
AUDIO_MUTE,
createShortcutEvent,
createToolbarEvent,
sendAnalytics
} from '../../analytics';
import { translate } from '../../base/i18n';
import { MEDIA_TYPE, setAudioMuted } from '../../base/media';
import { MEDIA_TYPE } from '../../base/media';
import { connect } from '../../base/redux';
import { AbstractAudioMuteButton } from '../../base/toolbox';
import type { AbstractButtonProps } from '../../base/toolbox';
import { isLocalTrackMuted } from '../../base/tracks';
import UIEvents from '../../../../service/UI/UIEvents';
import { muteLocal } from '../../remote-video-menu/actions';
declare var APP: Object;
@ -125,13 +124,7 @@ class AudioMuteButton extends AbstractAudioMuteButton<Props, *> {
* @returns {void}
*/
_setAudioMuted(audioMuted: boolean) {
sendAnalytics(createToolbarEvent(AUDIO_MUTE, { enable: audioMuted }));
this.props.dispatch(setAudioMuted(audioMuted, /* ensureTrack */ true));
// FIXME: The old conference logic as well as the shared video feature
// still rely on this event being emitted.
typeof APP === 'undefined'
|| APP.UI.emitEvent(UIEvents.AUDIO_MUTED, audioMuted, true);
this.props.dispatch(muteLocal(audioMuted));
}
/**

View File

@ -0,0 +1,75 @@
// @flow
import { createToolbarEvent, sendAnalytics } from '../../../analytics';
import { openDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { IconMicDisabled } from '../../../base/icons';
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox';
import { MuteEveryoneDialog } from '../../../remote-video-menu';
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 audio muting
* every participant (except the local one)
*/
class MuteEveryoneButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryone';
icon = IconMicDisabled;
label = 'toolbar.muteEveryone';
tooltip = 'toolbar.muteEveryone';
/**
* 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(MuteEveryoneDialog, {
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;
return {
isModerator,
localParticipantId: localParticipant.id,
visible: visible && isModerator
};
}
export default translate(connect(_mapStateToProps)(MuteEveryoneButton));

View File

@ -78,6 +78,7 @@ import HangupButton from '../HangupButton';
import HelpButton from '../HelpButton';
import OverflowMenuButton from './OverflowMenuButton';
import OverflowMenuProfileItem from './OverflowMenuProfileItem';
import MuteEveryoneButton from './MuteEveryoneButton';
import ToolbarButton from './ToolbarButton';
import VideoMuteButton from '../VideoMuteButton';
import {
@ -1000,6 +1001,10 @@ class Toolbox extends Component<Props, State> {
key = 'settings'
showLabel = { true }
visible = { this._shouldShowButton('settings') } />,
<MuteEveryoneButton
key = 'mute-everyone'
showLabel = { true }
visible = { true || this._shouldShowButton('mute-everyone') } />,
this._shouldShowButton('stats')
&& <OverflowMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.speakerStats') }