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
-
- { spotlightPeers.map((peer) => (
+ { participants.map((peer) => (
- roomClient.setSelectedPeer(peer.id)}
>
-
-
-
-
- ))}
-
-
- -
-
-
- { passivePeers.map((peer) => (
- - roomClient.setSelectedPeer(peer.id)}
- >
-
+ { spotlights.includes(peer.id) ?
+
+
+
+ :
+
+ }
))}
@@ -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);