diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index beea13b..1c93794 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -1533,6 +1533,26 @@ export default class RoomClient } } + async lowerPeerHand(peerId) + { + logger.debug('lowerPeerHand() [peerId:"%s"]', peerId); + + store.dispatch( + peerActions.setPeerRaisedHandInProgress(peerId, true)); + + try + { + await this.sendRequest('moderator:lowerHand', { peerId }); + } + catch (error) + { + logger.error('lowerPeerHand() | [error:"%o"]', error); + } + + store.dispatch( + peerActions.setPeerRaisedHandInProgress(peerId, false)); + } + async setRaisedHand(raisedHand) { logger.debug('setRaisedHand: ', raisedHand); @@ -2534,6 +2554,13 @@ export default class RoomClient break; } + case 'moderator:lowerHand': + { + this.setRaisedHand(false); + + break; + } + case 'gotRole': { const { peerId, role } = notification.data; diff --git a/app/src/actions/peerActions.js b/app/src/actions/peerActions.js index fee30a5..414b744 100644 --- a/app/src/actions/peerActions.js +++ b/app/src/actions/peerActions.js @@ -40,6 +40,12 @@ export const setPeerRaisedHand = (peerId, raisedHand, raisedHandTimestamp) => payload : { peerId, raisedHand, raisedHandTimestamp } }); +export const setPeerRaisedHandInProgress = (peerId, flag) => + ({ + type : 'SET_PEER_RAISED_HAND_IN_PROGRESS', + payload : { peerId, flag } + }); + export const setPeerPicture = (peerId, picture) => ({ type : 'SET_PEER_PICTURE', diff --git a/app/src/components/MeetingDrawer/ParticipantList/ListMe.js b/app/src/components/MeetingDrawer/ParticipantList/ListMe.js index d230db2..33873d2 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ListMe.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ListMe.js @@ -2,6 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../../RoomContext'; +import classnames from 'classnames'; import PropTypes from 'prop-types'; import * as appPropTypes from '../../appPropTypes'; import { useIntl } from 'react-intl'; @@ -23,7 +24,7 @@ const styles = (theme) => { borderRadius : '50%', height : '2rem', - marginTop : theme.spacing(1) + marginTop : theme.spacing(0.5) }, peerInfo : { @@ -33,6 +34,10 @@ const styles = (theme) => flexGrow : 1, alignItems : 'center' }, + buttons : + { + padding : theme.spacing(1) + }, green : { color : 'rgba(0, 153, 0, 1)' @@ -71,7 +76,9 @@ const ListMe = (props) => id : 'tooltip.raisedHand', defaultMessage : 'Raise hand' })} - className={me.raisedHand ? classes.green : null} + className={ + classnames(me.raisedHand ? classes.green : null, classes.buttons) + } disabled={me.raisedHandInProgress} color='primary' onClick={(e) => diff --git a/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js b/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js index 1aa70a1..d8b3fb3 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js @@ -6,6 +6,7 @@ import PropTypes from 'prop-types'; import * as appPropTypes from '../../appPropTypes'; import { withRoomContext } from '../../../RoomContext'; import { useIntl } from 'react-intl'; +import { green } from '@material-ui/core/colors'; import IconButton from '@material-ui/core/IconButton'; import Tooltip from '@material-ui/core/Tooltip'; import VideocamIcon from '@material-ui/icons/Videocam'; @@ -17,6 +18,7 @@ import ScreenOffIcon from '@material-ui/icons/StopScreenShare'; import ExitIcon from '@material-ui/icons/ExitToApp'; import EmptyAvatar from '../../../images/avatar-empty.jpeg'; import PanIcon from '@material-ui/icons/PanTool'; +import RecordVoiceOverIcon from '@material-ui/icons/RecordVoiceOver'; const styles = (theme) => ({ @@ -31,7 +33,7 @@ const styles = (theme) => { borderRadius : '50%', height : '2rem', - marginTop : theme.spacing(1) + marginTop : theme.spacing(0.5) }, peerInfo : { @@ -44,11 +46,16 @@ const styles = (theme) => indicators : { display : 'flex', - padding : theme.spacing(1.5) + padding : theme.spacing(1) + }, + buttons : + { + padding : theme.spacing(1) }, green : { - color : 'rgba(0, 153, 0, 1)' + color : 'rgba(0, 153, 0, 1)', + marginLeft : theme.spacing(2) } }); @@ -59,6 +66,7 @@ const ListPeer = (props) => const { roomClient, isModerator, + spotlight, peer, micConsumer, webcamConsumer, @@ -94,11 +102,30 @@ const ListPeer = (props) =>
{peer.displayName}
-
- { peer.raisedHand && - - } -
+ { peer.raisedHand && + + { + e.stopPropagation(); + + roomClient.lowerPeerHand(peer.id); + }} + > + + + } + { spotlight && + + + + } { screenConsumer && })} color={screenVisible ? 'primary' : 'secondary'} disabled={peer.peerScreenInProgress} + className={classes.buttons} onClick={(e) => { e.stopPropagation(); @@ -145,6 +173,7 @@ const ListPeer = (props) => })} color={webcamEnabled ? 'primary' : 'secondary'} disabled={peer.peerVideoInProgress} + className={classes.buttons} onClick={(e) => { e.stopPropagation(); @@ -175,6 +204,7 @@ const ListPeer = (props) => })} color={micEnabled ? 'primary' : 'secondary'} disabled={peer.peerAudioInProgress} + className={classes.buttons} onClick={(e) => { e.stopPropagation(); @@ -205,6 +235,7 @@ const ListPeer = (props) => defaultMessage : 'Kick out participant' })} disabled={peer.peerKickInProgress} + className={classes.buttons} color='secondary' onClick={(e) => { @@ -227,6 +258,7 @@ ListPeer.propTypes = roomClient : PropTypes.any.isRequired, advancedMode : PropTypes.bool, isModerator : PropTypes.bool, + spotlight : PropTypes.bool, peer : appPropTypes.Peer.isRequired, micConsumer : appPropTypes.Consumer, webcamConsumer : appPropTypes.Consumer, diff --git a/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js b/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js index af35dbd..411c745 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js @@ -1,10 +1,9 @@ import React from 'react'; import { connect } from 'react-redux'; import { - passivePeersSelector, - spotlightSortedPeersSelector + participantListSelector } from '../../Selectors'; -import classNames from 'classnames'; +import classnames from 'classnames'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../../RoomContext'; import PropTypes from 'prop-types'; @@ -76,9 +75,9 @@ class ParticipantList extends React.PureComponent roomClient, advancedMode, isModerator, - passivePeers, + participants, + spotlights, selectedPeerId, - spotlightPeers, classes } = this.props; @@ -107,48 +106,34 @@ class ParticipantList extends React.PureComponent - @@ -162,9 +147,9 @@ ParticipantList.propTypes = roomClient : PropTypes.any.isRequired, advancedMode : PropTypes.bool, isModerator : PropTypes.bool, - passivePeers : PropTypes.array, + participants : PropTypes.array, + spotlights : PropTypes.array, selectedPeerId : PropTypes.string, - spotlightPeers : PropTypes.array, classes : PropTypes.object.isRequired }; @@ -174,9 +159,9 @@ const mapStateToProps = (state) => isModerator : state.me.roles.some((role) => state.room.permissionsFromRoles.MODERATE_ROOM.includes(role)), - passivePeers : passivePeersSelector(state), - selectedPeerId : state.room.selectedPeerId, - spotlightPeers : spotlightSortedPeersSelector(state) + participants : participantListSelector(state), + spotlights : state.room.spotlights, + selectedPeerId : state.room.selectedPeerId }; }; diff --git a/app/src/components/Selectors.js b/app/src/components/Selectors.js index fd22aff..b8e5e41 100644 --- a/app/src/components/Selectors.js +++ b/app/src/components/Selectors.js @@ -12,7 +12,8 @@ const peersKeySelector = createSelector( peersSelector, (peers) => Object.keys(peers) ); -const peersValueSelector = createSelector( + +export const peersValueSelector = createSelector( peersSelector, (peers) => Object.values(peers) ); @@ -113,8 +114,31 @@ export const spotlightPeersSelector = createSelector( export const spotlightSortedPeersSelector = createSelector( spotlightsSelector, peersValueSelector, - (spotlights, peers) => peers.filter((peer) => spotlights.includes(peer.id)) - .sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || ''))) + (spotlights, peers) => + peers.filter((peer) => spotlights.includes(peer.id) && !peer.raisedHand) + .sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || ''))) +); + +const raisedHandSortedPeers = createSelector( + peersValueSelector, + (peers) => peers.filter((peer) => peer.raisedHand) + .sort((a, b) => a.raisedHandTimestamp - b.raisedHandTimestamp) +); + +const peersSortedSelector = createSelector( + spotlightsSelector, + peersValueSelector, + (spotlights, peers) => + peers.filter((peer) => !spotlights.includes(peer.id) && !peer.raisedHand) + .sort((a, b) => String(a.displayName || '').localeCompare(String(b.displayName || ''))) +); + +export const participantListSelector = createSelector( + raisedHandSortedPeers, + spotlightSortedPeersSelector, + peersSortedSelector, + (raisedHands, spotlights, peers) => + [ ...raisedHands, ...spotlights, ...peers ] ); export const peersLengthSelector = createSelector( diff --git a/app/src/reducers/peers.js b/app/src/reducers/peers.js index 4c8bee1..3e2c6a0 100644 --- a/app/src/reducers/peers.js +++ b/app/src/reducers/peers.js @@ -26,6 +26,12 @@ const peer = (state = {}, action) => raisedHand : action.payload.raisedHand, raisedHandTimestamp : action.payload.raisedHandTimestamp }; + + case 'SET_PEER_RAISED_HAND_IN_PROGRESS': + return { + ...state, + raisedHandInProgress : action.payload.flag + }; case 'ADD_CONSUMER': { @@ -91,6 +97,7 @@ const peers = (state = {}, action) => case 'SET_PEER_AUDIO_IN_PROGRESS': case 'SET_PEER_SCREEN_IN_PROGRESS': case 'SET_PEER_RAISED_HAND': + case 'SET_PEER_RAISED_HAND_IN_PROGRESS': case 'SET_PEER_PICTURE': case 'ADD_CONSUMER': case 'ADD_PEER_ROLE': diff --git a/server/lib/Room.js b/server/lib/Room.js index 0f89b4d..1b207fa 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -1430,6 +1430,29 @@ class Room extends EventEmitter break; } + case 'moderator:lowerHand': + { + if ( + !peer.roles.some( + (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) + ) + ) + throw new Error('peer not authorized'); + + const { peerId } = request.data; + + const lowerPeer = this._peers[peerId]; + + if (!lowerPeer) + throw new Error(`peer with id "${peerId}" not found`); + + this._notification(lowerPeer.socket, 'moderator:lowerHand'); + + cb(); + + break; + } + default: { logger.error('unknown request.method "%s"', request.method);