mirror of https://github.com/jitsi/jitsi-meet
feat: Added mute video moderation feature (#8630)
* Added mute video feature * Fixed export * Fixed some issues * Added remote video mute notification * Fixed import * Fixed conference event handling * Fixed some linting issues * Fixed more linter errors * turn screenshare off on remote video mute * Fix linter issue * translations added for mute video feature * Added video mute button to interface config * Updated lib-jitsi-meet * Fix copy paste error Co-authored-by: nurjinn jafar <nurjin.jafar@nordeck.net>pull/8681/head jitsi-meet_5545
parent
42d926eef3
commit
23bb824731
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,48 @@ |
||||
// @flow
|
||||
|
||||
import { createToolbarEvent, sendAnalytics } from '../../analytics'; |
||||
import { openDialog } from '../../base/dialog'; |
||||
import { IconMuteVideoEveryone } from '../../base/icons'; |
||||
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components'; |
||||
|
||||
import { MuteEveryonesVideoDialog } from '.'; |
||||
|
||||
export type Props = AbstractButtonProps & { |
||||
|
||||
/** |
||||
* The redux {@code dispatch} function. |
||||
*/ |
||||
dispatch: Function, |
||||
|
||||
/** |
||||
* The ID of the participant object that this button is supposed to keep unmuted. |
||||
*/ |
||||
participantID: string, |
||||
|
||||
/** |
||||
* The function to be used to translate i18n labels. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* An abstract remote video menu button which disables the camera of all the other participants. |
||||
*/ |
||||
export default class AbstractMuteEveryoneElsesVideoButton extends AbstractButton<Props, *> { |
||||
accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryoneElsesVideo'; |
||||
icon = IconMuteVideoEveryone; |
||||
label = 'videothumbnail.domuteVideoOfOthers'; |
||||
|
||||
/** |
||||
* Handles clicking / pressing the button, and opens a confirmation dialog. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_handleClick() { |
||||
const { dispatch, participantID } = this.props; |
||||
|
||||
sendAnalytics(createToolbarEvent('mute.everyoneelsesvideo.pressed')); |
||||
dispatch(openDialog(MuteEveryonesVideoDialog, { exclude: [ participantID ] })); |
||||
} |
||||
} |
@ -0,0 +1,103 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
|
||||
import { Dialog } from '../../base/dialog'; |
||||
import { MEDIA_TYPE } from '../../base/media'; |
||||
import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants'; |
||||
import { muteAllParticipants } from '../actions'; |
||||
|
||||
import AbstractMuteRemoteParticipantsVideoDialog, { |
||||
type Props as AbstractProps |
||||
} from './AbstractMuteRemoteParticipantsVideoDialog'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of |
||||
* {@link AbstractMuteEveryonesVideoDialog}. |
||||
*/ |
||||
export type Props = AbstractProps & { |
||||
|
||||
content: string, |
||||
exclude: Array<string>, |
||||
title: string |
||||
}; |
||||
|
||||
/** |
||||
* |
||||
* An abstract Component with the contents for a dialog that asks for confirmation |
||||
* from the user before disabling all remote participants cameras. |
||||
* |
||||
* @extends AbstractMuteRemoteParticipantsVideoDialog |
||||
*/ |
||||
export default class AbstractMuteEveryonesVideoDialog<P: Props> extends AbstractMuteRemoteParticipantsVideoDialog<P> { |
||||
static defaultProps = { |
||||
exclude: [], |
||||
muteLocal: false |
||||
}; |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { content, title } = this.props; |
||||
|
||||
return ( |
||||
<Dialog |
||||
okKey = 'dialog.muteParticipantsVideoButton' |
||||
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, 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') |
||||
}; |
||||
} |
@ -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<P:Props = Props> |
||||
extends Component<P> { |
||||
/** |
||||
* 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; |
||||
} |
||||
} |
@ -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<Props, *> { |
||||
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) |
||||
}; |
||||
} |
@ -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 ( |
||||
<RemoteVideoMenuButton |
||||
buttonText = { t('videothumbnail.domuteVideoOfOthers') } |
||||
displayClass = { 'mutelink' } |
||||
icon = { IconMuteVideoEveryoneElse } |
||||
id = { `mutelink_${participantID}` } |
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onClick = { this._handleClick } /> |
||||
); |
||||
} |
||||
|
||||
_handleClick: () => void; |
||||
} |
||||
|
||||
export default translate(connect()(MuteEveryoneElsesVideoButton)); |
@ -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<Props> { |
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
return ( |
||||
<Dialog |
||||
okKey = 'dialog.muteParticipantsVideoButton' |
||||
onSubmit = { this._onSubmit } |
||||
titleString = { this.props.title } |
||||
width = 'small'> |
||||
<div> |
||||
{ this.props.content } |
||||
</div> |
||||
</Dialog> |
||||
); |
||||
} |
||||
|
||||
_onSubmit: () => boolean; |
||||
} |
||||
|
||||
export default translate(connect(abstractMapStateToProps)(MuteEveryonesVideoDialog)); |
@ -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 ( |
||||
<Dialog |
||||
okKey = 'dialog.muteParticipantsVideoButton' |
||||
onSubmit = { this._onSubmit } |
||||
titleKey = 'dialog.muteParticipantsVideoTitle' |
||||
width = 'small'> |
||||
<div> |
||||
{ this.props.t('dialog.muteParticipantsVideoBody') } |
||||
</div> |
||||
</Dialog> |
||||
); |
||||
} |
||||
|
||||
_onSubmit: () => boolean; |
||||
} |
||||
|
||||
export default translate(connect()(MuteRemoteParticipantsVideoDialog)); |
@ -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 ( |
||||
<RemoteVideoMenuButton |
||||
buttonText = { t(muteConfig.translationKey) } |
||||
displayClass = { muteConfig.muteClassName } |
||||
icon = { IconCameraDisabled } |
||||
id = { `mutelink_${participantID}` } |
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onClick = { this._handleClick } /> |
||||
); |
||||
} |
||||
|
||||
_handleClick: () => void |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(MuteVideoButton)); |
@ -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<Props, *> { |
||||
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)); |
Loading…
Reference in new issue