mirror of https://github.com/jitsi/jitsi-meet
feat(av-moderation) Updated Advanced moderation (#9875)
Co-authored-by: Vlad Piersec <vlad.piersec@8x8.com>pull/9842/head jitsi-meet_6310
parent
f2e2d52cfd
commit
1dc8bfa631
@ -0,0 +1,19 @@ |
||||
.mute-dialog { |
||||
.separator-line { |
||||
margin: 24px 0 24px -20px; |
||||
padding: 0 20px; |
||||
width: 100%; |
||||
height: 1px; |
||||
background: #5E6D7A; |
||||
} |
||||
|
||||
.control-row { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
margin-top: 15px; |
||||
|
||||
label { |
||||
font-size: 14px; |
||||
} |
||||
} |
||||
} |
@ -1,38 +0,0 @@ |
||||
import React from 'react'; |
||||
import { useTranslation } from 'react-i18next'; |
||||
import { useSelector } from 'react-redux'; |
||||
|
||||
import NotificationWithParticipants from '../../notifications/components/web/NotificationWithParticipants'; |
||||
import { |
||||
approveParticipant, |
||||
dismissPendingAudioParticipant |
||||
} from '../actions'; |
||||
import { getParticipantsAskingToAudioUnmute } from '../functions'; |
||||
|
||||
|
||||
/** |
||||
* Component used to display a list of participants who asked to be unmuted. |
||||
* This is visible only to moderators. |
||||
* |
||||
* @returns {React$Element<'ul'> | null} |
||||
*/ |
||||
export default function() { |
||||
const participants = useSelector(getParticipantsAskingToAudioUnmute); |
||||
const { t } = useTranslation(); |
||||
|
||||
return participants.length |
||||
? ( |
||||
<> |
||||
<div className = 'title'> |
||||
{ t('raisedHand') } |
||||
</div> |
||||
<NotificationWithParticipants |
||||
approveButtonText = { t('notify.unmute') } |
||||
onApprove = { approveParticipant } |
||||
onReject = { dismissPendingAudioParticipant } |
||||
participants = { participants } |
||||
rejectButtonText = { t('dialog.dismiss') } |
||||
testIdPrefix = 'avModeration' /> |
||||
</> |
||||
) : null; |
||||
} |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB |
@ -0,0 +1,47 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
|
||||
import { LobbyParticipantItem } from './LobbyParticipantItem'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* Opens a drawer with actions for a knocking participant. |
||||
*/ |
||||
openDrawerForParticipant: Function, |
||||
|
||||
/** |
||||
* If a drawer with actions should be displayed. |
||||
*/ |
||||
overflowDrawer: boolean, |
||||
|
||||
/** |
||||
* List with the knocking participants. |
||||
*/ |
||||
participants: Array<Object> |
||||
} |
||||
|
||||
/** |
||||
* Component used to display a list of knocking participants. |
||||
* |
||||
* @param {Object} props - The props of the component. |
||||
* @returns {ReactNode} |
||||
*/ |
||||
function LobbyParticipantItems({ openDrawerForParticipant, overflowDrawer, participants }: Props) { |
||||
|
||||
return ( |
||||
<div> |
||||
{participants.map(p => ( |
||||
<LobbyParticipantItem |
||||
key = { p.id } |
||||
openDrawerForParticipant = { openDrawerForParticipant } |
||||
overflowDrawer = { overflowDrawer } |
||||
participant = { p } />) |
||||
)} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
// Memoize the component in order to avoid rerender on drawer open/close.
|
||||
export default React.memo<Props>(LobbyParticipantItems); |
@ -1,72 +0,0 @@ |
||||
// @flow
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles'; |
||||
import React, { useCallback } from 'react'; |
||||
import { useTranslation } from 'react-i18next'; |
||||
import { useSelector, useDispatch } from 'react-redux'; |
||||
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web'; |
||||
import { admitMultiple } from '../../../lobby/actions.web'; |
||||
import { getKnockingParticipants, getLobbyEnabled } from '../../../lobby/functions'; |
||||
|
||||
import { LobbyParticipantItem } from './LobbyParticipantItem'; |
||||
|
||||
const useStyles = makeStyles(theme => { |
||||
return { |
||||
headingContainer: { |
||||
alignItems: 'center', |
||||
display: 'flex', |
||||
justifyContent: 'space-between' |
||||
}, |
||||
heading: { |
||||
...withPixelLineHeight(theme.typography.heading7), |
||||
color: theme.palette.text02 |
||||
}, |
||||
link: { |
||||
...withPixelLineHeight(theme.typography.labelBold), |
||||
color: theme.palette.link01, |
||||
cursor: 'pointer' |
||||
} |
||||
}; |
||||
}); |
||||
|
||||
|
||||
export const LobbyParticipantList = () => { |
||||
const lobbyEnabled = useSelector(getLobbyEnabled); |
||||
const participants = useSelector(getKnockingParticipants); |
||||
|
||||
const { t } = useTranslation(); |
||||
const classes = useStyles(); |
||||
const dispatch = useDispatch(); |
||||
const admitAll = useCallback(() => { |
||||
dispatch(admitMultiple(participants)); |
||||
}, [ dispatch, participants ]); |
||||
|
||||
if (!lobbyEnabled || !participants.length) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
<div className = { classes.headingContainer }> |
||||
<div className = { classes.heading }> |
||||
{t('participantsPane.headings.lobby', { count: participants.length })} |
||||
</div> |
||||
{ |
||||
participants.length > 1 && ( |
||||
<div |
||||
className = { classes.link } |
||||
onClick = { admitAll }>{t('lobby.admitAll')}</div> |
||||
) |
||||
} |
||||
</div> |
||||
<div> |
||||
{participants.map(p => ( |
||||
<LobbyParticipantItem |
||||
key = { p.id } |
||||
participant = { p } />) |
||||
)} |
||||
</div> |
||||
</> |
||||
); |
||||
}; |
@ -0,0 +1,134 @@ |
||||
// @flow
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles'; |
||||
import React, { useCallback } from 'react'; |
||||
import { useTranslation } from 'react-i18next'; |
||||
import { useSelector, useDispatch } from 'react-redux'; |
||||
|
||||
import { Avatar } from '../../../base/avatar'; |
||||
import { Icon, IconCheck, IconClose } from '../../../base/icons'; |
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web'; |
||||
import { admitMultiple } from '../../../lobby/actions.web'; |
||||
import { getLobbyEnabled, getKnockingParticipants } from '../../../lobby/functions'; |
||||
import { Drawer, DrawerPortal } from '../../../toolbox/components/web'; |
||||
import { showOverflowDrawer } from '../../../toolbox/functions'; |
||||
import { useLobbyActions, useParticipantDrawer } from '../../hooks'; |
||||
|
||||
import LobbyParticipantItems from './LobbyParticipantItems'; |
||||
|
||||
const useStyles = makeStyles(theme => { |
||||
return { |
||||
drawerActions: { |
||||
listStyleType: 'none', |
||||
margin: 0, |
||||
padding: 0 |
||||
}, |
||||
drawerItem: { |
||||
alignItems: 'center', |
||||
color: theme.palette.text01, |
||||
display: 'flex', |
||||
padding: '12px 16px', |
||||
...withPixelLineHeight(theme.typography.bodyShortRegularLarge), |
||||
|
||||
'&:first-child': { |
||||
marginTop: '15px' |
||||
|
||||
}, |
||||
|
||||
'&:hover': { |
||||
cursor: 'pointer', |
||||
background: theme.palette.action02 |
||||
} |
||||
}, |
||||
icon: { |
||||
marginRight: 16 |
||||
}, |
||||
headingContainer: { |
||||
alignItems: 'center', |
||||
display: 'flex', |
||||
justifyContent: 'space-between' |
||||
}, |
||||
heading: { |
||||
...withPixelLineHeight(theme.typography.heading7), |
||||
color: theme.palette.text02 |
||||
}, |
||||
link: { |
||||
...withPixelLineHeight(theme.typography.labelBold), |
||||
color: theme.palette.link01, |
||||
cursor: 'pointer' |
||||
} |
||||
}; |
||||
}); |
||||
|
||||
/** |
||||
* Component used to display a list of participants waiting in the lobby. |
||||
* |
||||
* @returns {ReactNode} |
||||
*/ |
||||
export default function LobbyParticipants() { |
||||
const lobbyEnabled = useSelector(getLobbyEnabled); |
||||
const participants = useSelector(getKnockingParticipants); |
||||
const { t } = useTranslation(); |
||||
const classes = useStyles(); |
||||
const dispatch = useDispatch(); |
||||
const admitAll = useCallback(() => { |
||||
dispatch(admitMultiple(participants)); |
||||
}, [ dispatch, participants ]); |
||||
const overflowDrawer = useSelector(showOverflowDrawer); |
||||
const [ drawerParticipant, closeDrawer, openDrawerForParticipant ] = useParticipantDrawer(); |
||||
const [ admit, reject ] = useLobbyActions(drawerParticipant, closeDrawer); |
||||
|
||||
if (!lobbyEnabled || !participants.length) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
<div className = { classes.headingContainer }> |
||||
<div className = { classes.heading }> |
||||
{t('participantsPane.headings.lobby', { count: participants.length })} |
||||
</div> |
||||
<div |
||||
className = { classes.link } |
||||
onClick = { admitAll }>{t('lobby.admitAll')}</div> |
||||
</div> |
||||
<LobbyParticipantItems |
||||
openDrawerForParticipant = { openDrawerForParticipant } |
||||
overflowDrawer = { overflowDrawer } |
||||
participants = { participants } /> |
||||
<DrawerPortal> |
||||
<Drawer |
||||
isOpen = { Boolean(drawerParticipant && overflowDrawer) } |
||||
onClose = { closeDrawer }> |
||||
<ul className = { classes.drawerActions }> |
||||
<li className = { classes.drawerItem }> |
||||
<Avatar |
||||
className = { classes.icon } |
||||
participantId = { drawerParticipant && drawerParticipant.participantID } |
||||
size = { 20 } /> |
||||
<span>{ drawerParticipant && drawerParticipant.displayName }</span> |
||||
</li> |
||||
<li |
||||
className = { classes.drawerItem } |
||||
onClick = { admit }> |
||||
<Icon |
||||
className = { classes.icon } |
||||
size = { 20 } |
||||
src = { IconCheck } /> |
||||
<span>{ t('lobby.admit') }</span> |
||||
</li> |
||||
<li |
||||
className = { classes.drawerItem } |
||||
onClick = { reject }> |
||||
<Icon |
||||
className = { classes.icon } |
||||
size = { 20 } |
||||
src = { IconClose } /> |
||||
<span>{ t('lobby.reject')}</span> |
||||
</li> |
||||
</ul> |
||||
</Drawer> |
||||
</DrawerPortal> |
||||
</> |
||||
); |
||||
} |
@ -0,0 +1,103 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
|
||||
import MeetingParticipantItem from './MeetingParticipantItem'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* The translated ask unmute text for the qiuck action buttons. |
||||
*/ |
||||
askUnmuteText: string, |
||||
|
||||
/** |
||||
* Callback for the mouse leaving this item |
||||
*/ |
||||
lowerMenu: Function, |
||||
|
||||
/** |
||||
* Callback for the activation of this item's context menu |
||||
*/ |
||||
toggleMenu: Function, |
||||
|
||||
/** |
||||
* Callback used to open a confirmation dialog for audio muting. |
||||
*/ |
||||
muteAudio: Function, |
||||
|
||||
/** |
||||
* The translated text for the mute participant button. |
||||
*/ |
||||
muteParticipantButtonText: string, |
||||
|
||||
/** |
||||
* The meeting participants. |
||||
*/ |
||||
participantIds: Array<string>, |
||||
|
||||
/** |
||||
* Callback used to open an actions drawer for a participant. |
||||
*/ |
||||
openDrawerForParticipant: Function, |
||||
|
||||
/** |
||||
* True if an overflow drawer should be displayed. |
||||
*/ |
||||
overflowDrawer: boolean, |
||||
|
||||
/** |
||||
* The if of the participant for which the context menu should be open. |
||||
*/ |
||||
raiseContextId?: string, |
||||
|
||||
/** |
||||
* The aria-label for the ellipsis action. |
||||
*/ |
||||
participantActionEllipsisLabel: string, |
||||
|
||||
/** |
||||
* The translated "you" text. |
||||
*/ |
||||
youText: string |
||||
} |
||||
|
||||
/** |
||||
* Component used to display a list of meeting participants. |
||||
* |
||||
* @returns {ReactNode} |
||||
*/ |
||||
function MeetingParticipantItems({ |
||||
askUnmuteText, |
||||
lowerMenu, |
||||
toggleMenu, |
||||
muteAudio, |
||||
muteParticipantButtonText, |
||||
participantIds, |
||||
openDrawerForParticipant, |
||||
overflowDrawer, |
||||
raiseContextId, |
||||
participantActionEllipsisLabel, |
||||
youText |
||||
}) { |
||||
const renderParticipant = id => ( |
||||
<MeetingParticipantItem |
||||
askUnmuteText = { askUnmuteText } |
||||
isHighlighted = { raiseContextId === id } |
||||
key = { id } |
||||
muteAudio = { muteAudio } |
||||
muteParticipantButtonText = { muteParticipantButtonText } |
||||
onContextMenu = { toggleMenu(id) } |
||||
onLeave = { lowerMenu } |
||||
openDrawerForParticipant = { openDrawerForParticipant } |
||||
overflowDrawer = { overflowDrawer } |
||||
participantActionEllipsisLabel = { participantActionEllipsisLabel } |
||||
participantID = { id } |
||||
youText = { youText } /> |
||||
); |
||||
|
||||
return participantIds.map(renderParticipant); |
||||
} |
||||
|
||||
// Memoize the component in order to avoid rerender on drawer open/close.
|
||||
export default React.memo<Props>(MeetingParticipantItems); |
@ -1,7 +1,5 @@ |
||||
export * from './InviteButton'; |
||||
export * from './LobbyParticipantItem'; |
||||
export * from './LobbyParticipantList'; |
||||
export * from './MeetingParticipantList'; |
||||
export { default as ParticipantsPane } from './ParticipantsPane'; |
||||
export * from '../ParticipantsPaneButton'; |
||||
export * from './RaisedHandIndicator'; |
||||
|
@ -0,0 +1,46 @@ |
||||
import { useCallback, useState } from 'react'; |
||||
import { useDispatch } from 'react-redux'; |
||||
|
||||
import { approveKnockingParticipant, rejectKnockingParticipant } from '../lobby/actions'; |
||||
|
||||
/** |
||||
* Hook used to create admit/reject lobby actions. |
||||
* |
||||
* @param {Object} participant - The participant for which the actions are created. |
||||
* @param {Function} closeDrawer - Callback for closing the drawer. |
||||
* @returns {Array<Function>} |
||||
*/ |
||||
export function useLobbyActions(participant, closeDrawer) { |
||||
const dispatch = useDispatch(); |
||||
|
||||
return [ |
||||
useCallback(e => { |
||||
e.stopPropagation(); |
||||
dispatch(approveKnockingParticipant(participant && participant.participantID)); |
||||
closeDrawer && closeDrawer(); |
||||
}, [ dispatch, closeDrawer ]), |
||||
|
||||
useCallback(() => { |
||||
dispatch(rejectKnockingParticipant(participant && participant.participantID)); |
||||
closeDrawer && closeDrawer(); |
||||
}, [ dispatch, closeDrawer ]) |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* Hook used to create actions & state for opening a drawer. |
||||
* |
||||
* @returns {Array<any>} |
||||
*/ |
||||
export function useParticipantDrawer() { |
||||
const [ drawerParticipant, openDrawerForParticipant ] = useState(null); |
||||
const closeDrawer = useCallback(() => { |
||||
openDrawerForParticipant(null); |
||||
}); |
||||
|
||||
return [ |
||||
drawerParticipant, |
||||
closeDrawer, |
||||
openDrawerForParticipant |
||||
]; |
||||
} |
@ -1,32 +0,0 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
|
||||
import { ConfirmDialog } from '../../../base/dialog'; |
||||
import { translate } from '../../../base/i18n'; |
||||
import { connect } from '../../../base/redux'; |
||||
import AbstractMuteRemoteParticipantDialog |
||||
from '../AbstractMuteRemoteParticipantDialog'; |
||||
|
||||
/** |
||||
* Dialog to confirm a remote participant mute action. |
||||
*/ |
||||
class MuteRemoteParticipantDialog extends AbstractMuteRemoteParticipantDialog { |
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
return ( |
||||
<ConfirmDialog |
||||
contentKey = 'dialog.muteParticipantDialog' |
||||
onSubmit = { this._onSubmit } /> |
||||
); |
||||
} |
||||
|
||||
_onSubmit: () => boolean; |
||||
} |
||||
|
||||
export default translate(connect()(MuteRemoteParticipantDialog)); |
@ -1,41 +0,0 @@ |
||||
/* @flow */ |
||||
|
||||
import React from 'react'; |
||||
|
||||
import { Dialog } from '../../../base/dialog'; |
||||
import { translate } from '../../../base/i18n'; |
||||
import { connect } from '../../../base/redux'; |
||||
import AbstractMuteRemoteParticipantDialog |
||||
from '../AbstractMuteRemoteParticipantDialog'; |
||||
|
||||
/** |
||||
* A React Component with the contents for a dialog that asks for confirmation |
||||
* from the user before muting a remote participant. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class MuteRemoteParticipantDialog extends AbstractMuteRemoteParticipantDialog { |
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
return ( |
||||
<Dialog |
||||
okKey = 'dialog.muteParticipantButton' |
||||
onSubmit = { this._onSubmit } |
||||
titleKey = 'dialog.muteParticipantTitle' |
||||
width = 'small'> |
||||
<div> |
||||
{ this.props.t('dialog.muteParticipantBody') } |
||||
</div> |
||||
</Dialog> |
||||
); |
||||
} |
||||
|
||||
_onSubmit: () => boolean; |
||||
} |
||||
|
||||
export default translate(connect()(MuteRemoteParticipantDialog)); |
Loading…
Reference in new issue