feat: add grant moderator functionality
This commit is contained in:
parent
035f720a50
commit
b85cd2348f
|
@ -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",
|
||||
|
|
|
@ -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 |
|
@ -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';
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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'];
|
||||
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
|
@ -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));
|
|
@ -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 } />);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
// @flow
|
||||
|
||||
export {
|
||||
default as GrantModeratorDialog
|
||||
} from './GrantModeratorDialog';
|
||||
export {
|
||||
default as KickRemoteParticipantDialog
|
||||
} from './KickRemoteParticipantDialog';
|
||||
|
|
|
@ -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));
|
|
@ -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));
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue