Skip to content

Commit

Permalink
feat(raise-hand) add ability for the moderator to lower hands
Browse files Browse the repository at this point in the history
  • Loading branch information
liumengyuan1997 authored Jul 16, 2024
1 parent 74b02af commit 1376f59
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 5 deletions.
2 changes: 2 additions & 0 deletions lang/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,8 @@
"breakoutRooms": "Breakout rooms",
"goLive": "Go live",
"invite": "Invite Someone",
"lowerAllHands": "Lower all hands",
"lowerHand": "Lower the hand",
"moreModerationActions": "More moderation options",
"moreModerationControls": "More moderation controls",
"moreParticipantOptions": "More participant options",
Expand Down
1 change: 1 addition & 0 deletions react/features/base/tracks/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
* The payload name for remotely setting the camera facing mode message.
*/
export const CAMERA_FACING_MODE_MESSAGE = 'camera-facing-mode-message';
export const LOWER_HAND_MESSAGE = 'lower-hand-message';
15 changes: 13 additions & 2 deletions react/features/conference/middleware.any.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { IReduxState, IStore } from '../app/types';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT
CONFERENCE_LEFT,
ENDPOINT_MESSAGE_RECEIVED
} from '../base/conference/actionTypes';
import { getCurrentConference } from '../base/conference/functions';
import { getURLWithoutParamsNormalized } from '../base/connection/utils';
Expand All @@ -19,10 +20,11 @@ import { getLocalizedDateFormatter } from '../base/i18n/dateUtil';
import { translateToHTML } from '../base/i18n/functions';
import i18next from '../base/i18n/i18next';
import { browser } from '../base/lib-jitsi-meet';
import { pinParticipant, raiseHandClear } from '../base/participants/actions';
import { pinParticipant, raiseHand, raiseHandClear } from '../base/participants/actions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { SET_REDUCED_UI } from '../base/responsive-ui/actionTypes';
import { LOWER_HAND_MESSAGE } from '../base/tracks/constants';
import { BUTTON_TYPES } from '../base/ui/constants.any';
import { inIframe } from '../base/util/iframeUtils';
import { isCalendarEnabled } from '../calendar-sync/functions';
Expand Down Expand Up @@ -71,6 +73,15 @@ MiddlewareRegistry.register(store => next => action => {

break;
}
case ENDPOINT_MESSAGE_RECEIVED: {
const { participant, data } = action;
const { dispatch } = store;

if (data.name === LOWER_HAND_MESSAGE && participant.isModerator()) {
dispatch(raiseHand(false));
}
break;
}
}

return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ import {
isEnabled as isAvModerationEnabled,
isSupported as isAvModerationSupported
} from '../../../av-moderation/functions';
import { getCurrentConference } from '../../../base/conference/functions';
import { hideSheet, openDialog } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import Icon from '../../../base/icons/components/Icon';
import { IconCheck, IconVideoOff } from '../../../base/icons/svg';
import { IconCheck, IconRaiseHand, IconVideoOff } from '../../../base/icons/svg';
import { MEDIA_TYPE } from '../../../base/media/constants';
import { getParticipantCount, isEveryoneModerator } from '../../../base/participants/functions';
import { raiseHand } from '../../../base/participants/actions';
import { getParticipantCount, getRaiseHandsQueue, isEveryoneModerator, isLocalParticipantModerator }
from '../../../base/participants/functions';
import { LOWER_HAND_MESSAGE } from '../../../base/tracks/constants';
import MuteEveryonesVideoDialog
from '../../../video-menu/components/native/MuteEveryonesVideoDialog';

Expand All @@ -32,6 +36,14 @@ export const ContextMenuMore = () => {
dispatch(openDialog(MuteEveryonesVideoDialog));
dispatch(hideSheet());
}, [ dispatch ]);
const conference = useSelector(getCurrentConference);
const raisedHandsQueue = useSelector(getRaiseHandsQueue);
const moderator = useSelector(isLocalParticipantModerator);
const lowerAllHands = useCallback(() => {
dispatch(raiseHand(false));
conference?.sendEndpointMessage('', { name: LOWER_HAND_MESSAGE });
dispatch(hideSheet());
}, [ dispatch ]);
const { t } = useTranslation();

const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state));
Expand Down Expand Up @@ -59,6 +71,14 @@ export const ContextMenuMore = () => {
src = { IconVideoOff } />
<Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.stopEveryonesVideo')}</Text>
</TouchableOpacity>
{ moderator && raisedHandsQueue.length !== 0 && <TouchableOpacity
onPress = { lowerAllHands }
style = { styles.contextMenuItem as ViewStyle }>
<Icon
size = { 24 }
src = { IconRaiseHand } />
<Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.lowerAllHands')}</Text>
</TouchableOpacity> }
{isModerationSupported && ((participantCount === 1 || !allModerators)) && <>
{/* @ts-ignore */}
<Divider style = { styles.divider } />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { MEDIA_TYPE } from '../../../base/media/constants';
import {
getParticipantCount,
getRaiseHandsQueue,
isEveryoneModerator
} from '../../../base/participants/functions';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
Expand All @@ -32,6 +33,7 @@ import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { openSettingsDialog } from '../../../settings/actions.web';
import { SETTINGS_TABS } from '../../../settings/constants';
import { shouldShowModeratorSettings } from '../../../settings/functions.web';
import LowerHandButton from '../../../video-menu/components/web/LowerHandButton';
import MuteEveryonesVideoDialog from '../../../video-menu/components/web/MuteEveryonesVideoDialog';

const useStyles = makeStyles()(theme => {
Expand Down Expand Up @@ -85,6 +87,7 @@ interface IProps {
export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: IProps) => {
const dispatch = useDispatch();
const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state));
const raisedHandsQueue = useSelector(getRaiseHandsQueue);
const allModerators = useSelector(isEveryoneModerator);
const isModeratorSettingsTabEnabled = useSelector(shouldShowModeratorSettings);
const participantCount = useSelector(getParticipantCount);
Expand Down Expand Up @@ -147,6 +150,7 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: IProp
onClick: muteAllVideo,
text: t('participantsPane.actions.stopEveryonesVideo')
} ] } />
{raisedHandsQueue.length !== 0 && <LowerHandButton />}
{!isBreakoutRoom && isModerationSupported && (participantCount === 1 || !allModerators) && (
<ContextMenuItemGroup actions = { actions }>
<div className = { classes.text }>
Expand Down
71 changes: 71 additions & 0 deletions react/features/video-menu/components/native/LowerHandButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { connect } from 'react-redux';

import { IReduxState } from '../../../app/types';
import { getCurrentConference } from '../../../base/conference/functions';
import { IJitsiConference } from '../../../base/conference/reducer';
import { translate } from '../../../base/i18n/functions';
import { IconRaiseHand } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { LOWER_HAND_MESSAGE } from '../../../base/tracks/constants';

interface IProps extends AbstractButtonProps {

/**
* The current conference.
*/
_conference: IJitsiConference | undefined;

/**
* The ID of the participant object that this button is supposed to
* ask to lower the hand.
*/
participantId: String | undefined;
}

/**
* Implements a React {@link Component} which displays a button for lowering certain
* participant raised hands.
*
* @returns {JSX.Element}
*/
class LowerHandButton extends AbstractButton<IProps> {
icon = IconRaiseHand;
accessibilityLabel = 'participantsPane.actions.lowerHand';
label = 'participantsPane.actions.lowerHand';

/**
* Handles clicking / pressing the button, and asks the participant to lower hand.
*
* @private
* @returns {void}
*/
_handleClick() {
const { participantId, _conference } = this.props;

_conference?.sendEndpointMessage(
participantId,
{
name: LOWER_HAND_MESSAGE
}
);
}
}

/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - Properties of component.
* @returns {IProps}
*/
function mapStateToProps(state: IReduxState, ownProps: any) {
const { participantID } = ownProps;
const currentConference = getCurrentConference(state);

return {
_conference: currentConference,
participantId: participantID
};
}

export default translate(connect(mapStateToProps)(LowerHandButton));
11 changes: 11 additions & 0 deletions react/features/video-menu/components/native/RemoteVideoMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { translate } from '../../../base/i18n/functions';
import {
getParticipantById,
getParticipantDisplayName,
hasRaisedHand,
isLocalParticipantModerator
} from '../../../base/participants/functions';
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
Expand All @@ -27,6 +28,7 @@ import ConnectionStatusButton from './ConnectionStatusButton';
import DemoteToVisitorButton from './DemoteToVisitorButton';
import GrantModeratorButton from './GrantModeratorButton';
import KickButton from './KickButton';
import LowerHandButton from './LowerHandButton';
import MuteButton from './MuteButton';
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
import MuteVideoButton from './MuteVideoButton';
Expand Down Expand Up @@ -93,6 +95,11 @@ interface IProps {
*/
_participantDisplayName: string;

/**
* Whether the targeted participant raised hand or not.
*/
_raisedHand: boolean;

/**
* Array containing the breakout rooms.
*/
Expand Down Expand Up @@ -150,6 +157,7 @@ class RemoteVideoMenu extends PureComponent<IProps> {
_isParticipantAvailable,
_isParticipantSilent,
_moderator,
_raisedHand,
_rooms,
_showDemote,
_currentRoomId,
Expand All @@ -175,6 +183,7 @@ class RemoteVideoMenu extends PureComponent<IProps> {
{!_isParticipantSilent && <AskUnmuteButton { ...buttonProps } />}
{ !_disableRemoteMute && <MuteButton { ...buttonProps } /> }
<MuteEveryoneElseButton { ...buttonProps } />
{ _moderator && _raisedHand && <LowerHandButton { ...buttonProps } /> }
{ !_disableRemoteMute && !_isParticipantSilent && <MuteVideoButton { ...buttonProps } /> }
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
Expand Down Expand Up @@ -256,6 +265,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
const moderator = isLocalParticipantModerator(state);
const _iAmVisitor = state['features/visitors'].iAmVisitor;
const _isBreakoutRoom = isInBreakoutRoom(state);
const raisedHand = hasRaisedHand(participant);

return {
_currentRoomId,
Expand All @@ -267,6 +277,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
_isParticipantSilent: Boolean(participant?.isSilent),
_moderator: moderator,
_participantDisplayName: getParticipantDisplayName(state, participantId),
_raisedHand: raisedHand,
_rooms,
_showDemote: moderator
};
Expand Down
56 changes: 56 additions & 0 deletions react/features/video-menu/components/web/LowerHandButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';

import { getCurrentConference } from '../../../base/conference/functions';
import { IconRaiseHand } from '../../../base/icons/svg';
import { raiseHand } from '../../../base/participants/actions';
import { LOWER_HAND_MESSAGE } from '../../../base/tracks/constants';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';

interface IProps {

/**
* The ID of the participant that's linked to the button.
*/
participantID?: String;
}

/**
* Implements a React {@link Component} which displays a button for notifying certain
* participant who raised hand to lower hand.
*
* @returns {JSX.Element}
*/
const LowerHandButton = ({
participantID = ''
}: IProps): JSX.Element => {
const { t } = useTranslation();
const dispatch = useDispatch();
const currentConference = useSelector(getCurrentConference);
const accessibilityText = participantID
? t('participantsPane.actions.lowerHand')
: t('participantsPane.actions.lowerAllHands');

const handleClick = useCallback(() => {
if (!participantID) {
dispatch(raiseHand(false));
}
currentConference?.sendEndpointMessage(
participantID,
{
name: LOWER_HAND_MESSAGE
}
);
}, [ participantID ]);

return (
<ContextMenuItem
accessibilityLabel = { accessibilityText }
icon = { IconRaiseHand }
onClick = { handleClick }
text = { accessibilityText } />
);
};

export default LowerHandButton;
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Avatar from '../../../base/avatar/components/Avatar';
import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/utils';
import { MEDIA_TYPE } from '../../../base/media/constants';
import { PARTICIPANT_ROLE } from '../../../base/participants/constants';
import { getLocalParticipant } from '../../../base/participants/functions';
import { getLocalParticipant, hasRaisedHand } from '../../../base/participants/functions';
import { IParticipant } from '../../../base/participants/types';
import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks/functions.any';
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
Expand All @@ -33,6 +33,7 @@ import CustomOptionButton from './CustomOptionButton';
import DemoteToVisitorButton from './DemoteToVisitorButton';
import GrantModeratorButton from './GrantModeratorButton';
import KickButton from './KickButton';
import LowerHandButton from './LowerHandButton';
import MuteButton from './MuteButton';
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
import MuteEveryoneElsesVideoButton from './MuteEveryoneElsesVideoButton';
Expand Down Expand Up @@ -148,6 +149,7 @@ const ParticipantContextMenu = ({
: participant?.id ? participantsVolume[participant?.id] : undefined) ?? 1;
const isBreakoutRoom = useSelector(isInBreakoutRoom);
const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state));
const raisedHands = hasRaisedHand(participant);
const stageFilmstrip = useSelector(isStageFilmstripAvailable);
const shouldDisplayVerification = useSelector((state: IReduxState) => displayVerification(state, participant?.id));
const buttonsWithNotifyClick = useSelector(getParticipantMenuButtonsWithNotifyClick);
Expand Down Expand Up @@ -241,6 +243,10 @@ const ParticipantContextMenu = ({
buttons.push(<MuteEveryoneElsesVideoButton { ...getButtonProps(BUTTONS.MUTE_OTHERS_VIDEO) } />);
}

if (raisedHands) {
buttons2.push(<LowerHandButton { ...getButtonProps(BUTTONS.LOWER_PARTICIPANT_HAND) } />);
}

if (!disableGrantModerator && !isBreakoutRoom) {
buttons2.push(<GrantModeratorButton { ...getButtonProps(BUTTONS.GRANT_MODERATOR) } />);
}
Expand Down
1 change: 1 addition & 0 deletions react/features/video-menu/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const PARTICIPANT_MENU_BUTTONS = {
GRANT_MODERATOR: 'grant-moderator',
HIDE_SELF_VIEW: 'hide-self-view',
KICK: 'kick',
LOWER_PARTICIPANT_HAND: 'lower-participant-hand',
MUTE: 'mute',
MUTE_OTHERS: 'mute-others',
MUTE_OTHERS_VIDEO: 'mute-others-video',
Expand Down

0 comments on commit 1376f59

Please sign in to comment.