feat: add grant moderator functionality

This commit is contained in:
Gabriel Imre 2020-07-15 13:13:28 +03:00 committed by GitHub
parent 035f720a50
commit b85cd2348f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 356 additions and 8 deletions

View File

@ -203,6 +203,8 @@
"enterDisplayName": "Please enter your name here",
"error": "Error",
"gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
"grantModeratorDialog": "Are you sure you want to make this participant a moderator?",
"grantModeratorTitle": "Grant moderator",
"IamHost": "I am the host",
"incorrectRoomLockPassword": "Incorrect password",
"incorrectPassword": "Incorrect username or password",
@ -669,6 +671,7 @@
"e2ee": "End-to-End Encryption",
"feedback": "Leave feedback",
"fullScreen": "Toggle full screen",
"grantModerator": "Grant Moderator",
"hangup": "Leave the call",
"help": "Help",
"invite": "Invite people",
@ -817,6 +820,7 @@
"domute": "Mute",
"domuteOthers": "Mute everyone else",
"flip": "Flip",
"grantModerator": "Grant Moderator",
"kick": "Kick out",
"moderator": "Moderator",
"mute": "Participant is muted",

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 4C14 4.85739 13.4605 5.58876 12.7024 5.87317L14.2286 9.94296L14.9455 11.8546C15.0074 11.9292 15.0708 11.9292 15.1098 11.8902L16.5535 10.4465L18.5858 8.41421C18.2239 8.05228 18 7.55228 18 7C18 5.89543 18.8954 5 20 5C21.1046 5 22 5.89543 22 7C22 8.10457 21.1046 9 20 9C19.9441 9 19.8887 8.9977 19.8339 8.9932L19 19C19 20.1046 18.1046 21 17 21H7C5.89543 21 5 20.1046 5 19L4.1661 8.9932C4.11133 8.9977 4.05593 9 4 9C2.89543 9 2 8.10457 2 7C2 5.89543 2.89543 5 4 5C5.10457 5 6 5.89543 6 7C6 7.55228 5.77614 8.05228 5.41421 8.41421L7.44654 10.4465L8.89019 11.8902C8.9775 11.9325 9.03514 11.9063 9.05453 11.8546L9.77139 9.94296L11.2976 5.87317C10.5395 5.58876 10 4.85739 10 4C10 2.89543 10.8954 2 12 2C13.1046 2 14 2.89543 14 4ZM6.84027 17L6.44651 12.2749L7.47597 13.3044C7.68795 13.5164 7.94285 13.6805 8.22354 13.7858C9.30949 14.193 10.52 13.6428 10.9272 12.5568L12 9.696L13.0728 12.5568C13.1781 12.8375 13.3422 13.0924 13.5542 13.3044C14.3743 14.1245 15.7039 14.1245 16.524 13.3044L17.5535 12.2749L17.1597 17H6.84027Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -23,6 +23,7 @@ export { default as IconClosedCaption } from './closed_caption.svg';
export { default as IconConnectionActive } from './gsm-bars.svg';
export { default as IconConnectionInactive } from './ninja.svg';
export { default as IconCopy } from './copy.svg';
export { default as IconCrown } from './crown.svg';
export { default as IconDeviceBluetooth } from './bluetooth.svg';
export { default as IconDeviceEarpiece } from './phone-talk.svg';
export { default as IconDeviceHeadphone } from './headset.svg';

View File

@ -12,6 +12,16 @@
*/
export const DOMINANT_SPEAKER_CHANGED = 'DOMINANT_SPEAKER_CHANGED';
/**
* Create an action for granting moderator to a participant.
*
* {
* type: GRANT_MODERATOR,
* id: string
* }
*/
export const GRANT_MODERATOR = 'GRANT_MODERATOR';
/**
* Create an action for removing a participant from the conference.
*

View File

@ -5,6 +5,7 @@ import {
DOMINANT_SPEAKER_CHANGED,
HIDDEN_PARTICIPANT_JOINED,
HIDDEN_PARTICIPANT_LEFT,
GRANT_MODERATOR,
KICK_PARTICIPANT,
MUTE_REMOTE_PARTICIPANT,
PARTICIPANT_ID_CHANGED,
@ -47,6 +48,22 @@ export function dominantSpeakerChanged(id, conference) {
};
}
/**
* Create an action for granting moderator to a participant.
*
* @param {string} id - Participant's ID.
* @returns {{
* type: GRANT_MODERATOR,
* id: string
* }}
*/
export function grantModerator(id) {
return {
type: GRANT_MODERATOR,
id
};
}
/**
* Create an action for removing a participant from the conference.
*

View File

@ -259,6 +259,16 @@ export function getYoutubeParticipant(stateful: Object | Function) {
return participants.filter(p => p.isFakeParticipant)[0];
}
/**
* Returns true if the participant is a moderator.
*
* @param {string} participant - Participant object.
* @returns {boolean}
*/
export function isParticipantModerator(participant: Object) {
return participant?.role === PARTICIPANT_ROLE.MODERATOR;
}
/**
* Returns true if all of the meeting participants are moderators.
*
@ -269,13 +279,7 @@ export function getYoutubeParticipant(stateful: Object | Function) {
export function isEveryoneModerator(stateful: Object | Function) {
const participants = _getAllParticipants(stateful);
for (const participant of participants) {
if (participant.role !== PARTICIPANT_ROLE.MODERATOR) {
return false;
}
}
return true;
return participants.every(isParticipantModerator);
}
/**

View File

@ -15,6 +15,7 @@ import { playSound, registerSound, unregisterSound } from '../sounds';
import {
DOMINANT_SPEAKER_CHANGED,
GRANT_MODERATOR,
KICK_PARTICIPANT,
MUTE_REMOTE_PARTICIPANT,
PARTICIPANT_DISPLAY_NAME_CHANGED,
@ -86,6 +87,13 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case GRANT_MODERATOR: {
const { conference } = store.getState()['features/base/conference'];
conference.grantOwner(action.id);
break;
}
case KICK_PARTICIPANT: {
const { conference } = store.getState()['features/base/conference'];

View File

@ -36,6 +36,11 @@ type Props = {
*/
_localUserId: string,
/**
* The local participant's role.
*/
_localUserRole: string,
/**
* Indicates whether or not the test mode is currently on. Otherwise the
* TestConnectionInfo component will not render.
@ -179,6 +184,9 @@ class TestConnectionInfo extends Component<Props, State> {
<TestHint
id = 'org.jitsi.meet.conference.joinedState'
value = { this.props._conferenceJoinedState } />
<TestHint
id = 'org.jitsi.meet.conference.localParticipantRole'
value = { this.props._localUserRole } />
<TestHint
id = 'org.jitsi.meet.stats.rtp'
value = { JSON.stringify(this.state.stats) } />
@ -208,7 +216,8 @@ function _mapStateToProps(state) {
return {
_conferenceConnectionState: state['features/testing'].connectionState,
_conferenceJoinedState: conferenceJoined.toString(),
_localUserId: localParticipant && localParticipant.id,
_localUserId: localParticipant?.id,
_localUserRole: localParticipant?.role,
_testMode: isTestModeEnabled(state)
};
}

View File

@ -0,0 +1,70 @@
// @flow
import { openDialog } from '../../base/dialog';
import { IconCrown } from '../../base/icons';
import {
getParticipantById,
isLocalParticipantModerator,
isParticipantModerator
} from '../../base/participants';
import { AbstractButton } from '../../base/toolbox';
import type { AbstractButtonProps } from '../../base/toolbox';
import { GrantModeratorDialog } from '.';
export type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* The ID of the participant for whom to grant moderator status.
*/
participantID: string,
/**
* The function to be used to translate i18n labels.
*/
t: Function
};
/**
* An abstract remote video menu button which kicks the remote participant.
*/
export default class AbstractGrantModeratorButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.grantModerator';
icon = IconCrown;
label = 'videothumbnail.grantModerator';
/**
* Handles clicking / pressing the button, and kicks the participant.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participantID } = this.props;
dispatch(openDialog(GrantModeratorDialog, { participantID }));
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @param {Object} ownProps - Properties of component.
* @private
* @returns {{
* visible: boolean
* }}
*/
export function _mapStateToProps(state: Object, ownProps: Props) {
const { participantID } = ownProps;
return {
visible: isLocalParticipantModerator(state) && !isParticipantModerator(getParticipantById(state, participantID))
};
}

View File

@ -0,0 +1,66 @@
// @flow
import { Component } from 'react';
import {
createRemoteVideoMenuButtonEvent,
sendAnalytics
} from '../../analytics';
import { grantModerator } from '../../base/participants';
type Props = {
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* The ID of the remote participant to be granted moderator rights.
*/
participantID: string,
/**
* Function to translate i18n labels.
*/
t: Function
};
/**
* Abstract dialog to confirm granting moderator to a participant.
*/
export default class AbstractGrantModeratorDialog
extends Component<Props> {
/**
* Initializes a new {@code AbstractGrantModeratorDialog} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onSubmit = this._onSubmit.bind(this);
}
_onSubmit: () => boolean;
/**
* Callback for the confirm button.
*
* @private
* @returns {boolean} - True (to note that the modal should be closed).
*/
_onSubmit() {
const { dispatch, participantID } = this.props;
sendAnalytics(createRemoteVideoMenuButtonEvent(
'grant.moderator.button',
{
'participant_id': participantID
}));
dispatch(grantModerator(participantID));
return true;
}
}

View File

@ -0,0 +1,9 @@
// @flow
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractGrantModeratorButton, {
_mapStateToProps
} from '../AbstractGrantModeratorButton';
export default translate(connect(_mapStateToProps)(AbstractGrantModeratorButton));

View File

@ -0,0 +1,32 @@
// @flow
import React from 'react';
import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractGrantModeratorDialog
from '../AbstractGrantModeratorDialog';
/**
* Dialog to confirm a remote participant kick action.
*/
class GrantModeratorDialog extends AbstractGrantModeratorDialog {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<ConfirmDialog
contentKey = 'dialog.grantModeratorDialog'
onSubmit = { this._onSubmit } />
);
}
_onSubmit: () => boolean;
}
export default translate(connect()(GrantModeratorDialog));

View File

@ -12,6 +12,7 @@ import { StyleType } from '../../../base/styles';
import { PrivateMessageButton } from '../../../chat';
import { hideRemoteVideoMenu } from '../../actions';
import GrantModeratorButton from './GrantModeratorButton';
import KickButton from './KickButton';
import MuteButton from './MuteButton';
import PinButton from './PinButton';
@ -98,6 +99,8 @@ class RemoteVideoMenu extends Component<Props> {
buttons.push(<MuteButton { ...buttonProps } />);
}
buttons.push(<GrantModeratorButton { ...buttonProps } />);
if (!_disableKick) {
buttons.push(<KickButton { ...buttonProps } />);
}

View File

@ -1,5 +1,8 @@
// @flow
export {
default as GrantModeratorDialog
} from './GrantModeratorDialog';
export {
default as KickRemoteParticipantDialog
} from './KickRemoteParticipantDialog';

View File

@ -0,0 +1,60 @@
/* @flow */
import React from 'react';
import { translate } from '../../../base/i18n';
import { IconCrown } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractGrantModeratorButton, {
_mapStateToProps,
type Props
} from '../AbstractGrantModeratorButton';
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
declare var interfaceConfig: Object;
/**
* Implements a React {@link Component} which displays a button for granting
* moderator to a participant.
*/
class GrantModeratorButton extends AbstractGrantModeratorButton {
/**
* Instantiates a new {@code GrantModeratorButton}.
*
* @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, visible } = this.props;
if (!visible) {
return null;
}
return (
<RemoteVideoMenuButton
buttonText = { t('videothumbnail.grantModerator') }
displayClass = 'grantmoderatorlink'
icon = { IconCrown }
id = { `grantmoderatorlink_${participantID}` }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick } />
);
}
_handleClick: () => void
}
export default translate(connect(_mapStateToProps)(GrantModeratorButton));

View File

@ -0,0 +1,38 @@
// @flow
import React from 'react';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractGrantModeratorDialog
from '../AbstractGrantModeratorDialog';
/**
* Dialog to confirm a grant moderator action.
*/
class GrantModeratorDialog extends AbstractGrantModeratorDialog {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<Dialog
okKey = 'dialog.Yes'
onSubmit = { this._onSubmit }
titleKey = 'dialog.grantModeratorTitle'
width = 'small'>
<div>
{ this.props.t('dialog.grantModeratorDialog') }
</div>
</Dialog>
);
}
_onSubmit: () => boolean;
}
export default translate(connect()(GrantModeratorDialog));

View File

@ -8,6 +8,7 @@ import { Popover } from '../../../base/popover';
import { connect } from '../../../base/redux';
import {
GrantModeratorButton,
MuteButton,
MuteEveryoneElseButton,
KickButton,
@ -195,6 +196,12 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
);
}
buttons.push(
<GrantModeratorButton
key = 'grant-moderator'
participantID = { participantID } />
);
if (!_disableKick) {
buttons.push(
<KickButton

View File

@ -1,5 +1,9 @@
// @flow
export { default as GrantModeratorButton } from './GrantModeratorButton';
export {
default as GrantModeratorDialog
} from './GrantModeratorDialog';
export { default as KickButton } from './KickButton';
export {
default as KickRemoteParticipantDialog