Browse Source

feat(raise-hand) add ability for the moderator to lower hands

factor2
Mengyuan Liu 1 year ago
parent
commit
1376f5909c
No account linked to committer's email address

+ 2
- 0
lang/main.json View File

@@ -839,6 +839,8 @@
839 839
             "breakoutRooms": "Breakout rooms",
840 840
             "goLive": "Go live",
841 841
             "invite": "Invite Someone",
842
+            "lowerAllHands": "Lower all hands",
843
+            "lowerHand": "Lower the hand",
842 844
             "moreModerationActions": "More moderation options",
843 845
             "moreModerationControls": "More moderation controls",
844 846
             "moreParticipantOptions": "More participant options",

+ 1
- 0
react/features/base/tracks/constants.ts View File

@@ -2,3 +2,4 @@
2 2
  * The payload name for remotely setting the camera facing mode message.
3 3
  */
4 4
 export const CAMERA_FACING_MODE_MESSAGE = 'camera-facing-mode-message';
5
+export const LOWER_HAND_MESSAGE = 'lower-hand-message';

+ 13
- 2
react/features/conference/middleware.any.ts View File

@@ -9,7 +9,8 @@ import { IReduxState, IStore } from '../app/types';
9 9
 import {
10 10
     CONFERENCE_FAILED,
11 11
     CONFERENCE_JOINED,
12
-    CONFERENCE_LEFT
12
+    CONFERENCE_LEFT,
13
+    ENDPOINT_MESSAGE_RECEIVED
13 14
 } from '../base/conference/actionTypes';
14 15
 import { getCurrentConference } from '../base/conference/functions';
15 16
 import { getURLWithoutParamsNormalized } from '../base/connection/utils';
@@ -19,10 +20,11 @@ import { getLocalizedDateFormatter } from '../base/i18n/dateUtil';
19 20
 import { translateToHTML } from '../base/i18n/functions';
20 21
 import i18next from '../base/i18n/i18next';
21 22
 import { browser } from '../base/lib-jitsi-meet';
22
-import { pinParticipant, raiseHandClear } from '../base/participants/actions';
23
+import { pinParticipant, raiseHand, raiseHandClear } from '../base/participants/actions';
23 24
 import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
24 25
 import StateListenerRegistry from '../base/redux/StateListenerRegistry';
25 26
 import { SET_REDUCED_UI } from '../base/responsive-ui/actionTypes';
27
+import { LOWER_HAND_MESSAGE } from '../base/tracks/constants';
26 28
 import { BUTTON_TYPES } from '../base/ui/constants.any';
27 29
 import { inIframe } from '../base/util/iframeUtils';
28 30
 import { isCalendarEnabled } from '../calendar-sync/functions';
@@ -71,6 +73,15 @@ MiddlewareRegistry.register(store => next => action => {
71 73
 
72 74
         break;
73 75
     }
76
+    case ENDPOINT_MESSAGE_RECEIVED: {
77
+        const { participant, data } = action;
78
+        const { dispatch } = store;
79
+
80
+        if (data.name === LOWER_HAND_MESSAGE && participant.isModerator()) {
81
+            dispatch(raiseHand(false));
82
+        }
83
+        break;
84
+    }
74 85
     }
75 86
 
76 87
     return result;

+ 22
- 2
react/features/participants-pane/components/native/ContextMenuMore.tsx View File

@@ -15,12 +15,16 @@ import {
15 15
     isEnabled as isAvModerationEnabled,
16 16
     isSupported as isAvModerationSupported
17 17
 } from '../../../av-moderation/functions';
18
+import { getCurrentConference } from '../../../base/conference/functions';
18 19
 import { hideSheet, openDialog } from '../../../base/dialog/actions';
19 20
 import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
20 21
 import Icon from '../../../base/icons/components/Icon';
21
-import { IconCheck, IconVideoOff } from '../../../base/icons/svg';
22
+import { IconCheck, IconRaiseHand, IconVideoOff } from '../../../base/icons/svg';
22 23
 import { MEDIA_TYPE } from '../../../base/media/constants';
23
-import { getParticipantCount, isEveryoneModerator } from '../../../base/participants/functions';
24
+import { raiseHand } from '../../../base/participants/actions';
25
+import { getParticipantCount, getRaiseHandsQueue, isEveryoneModerator, isLocalParticipantModerator }
26
+    from '../../../base/participants/functions';
27
+import { LOWER_HAND_MESSAGE } from '../../../base/tracks/constants';
24 28
 import MuteEveryonesVideoDialog
25 29
     from '../../../video-menu/components/native/MuteEveryonesVideoDialog';
26 30
 
@@ -32,6 +36,14 @@ export const ContextMenuMore = () => {
32 36
         dispatch(openDialog(MuteEveryonesVideoDialog));
33 37
         dispatch(hideSheet());
34 38
     }, [ dispatch ]);
39
+    const conference = useSelector(getCurrentConference);
40
+    const raisedHandsQueue = useSelector(getRaiseHandsQueue);
41
+    const moderator = useSelector(isLocalParticipantModerator);
42
+    const lowerAllHands = useCallback(() => {
43
+        dispatch(raiseHand(false));
44
+        conference?.sendEndpointMessage('', { name: LOWER_HAND_MESSAGE });
45
+        dispatch(hideSheet());
46
+    }, [ dispatch ]);
35 47
     const { t } = useTranslation();
36 48
 
37 49
     const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state));
@@ -59,6 +71,14 @@ export const ContextMenuMore = () => {
59 71
                     src = { IconVideoOff } />
60 72
                 <Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.stopEveryonesVideo')}</Text>
61 73
             </TouchableOpacity>
74
+            { moderator && raisedHandsQueue.length !== 0 && <TouchableOpacity
75
+                onPress = { lowerAllHands }
76
+                style = { styles.contextMenuItem as ViewStyle }>
77
+                <Icon
78
+                    size = { 24 }
79
+                    src = { IconRaiseHand } />
80
+                <Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.lowerAllHands')}</Text>
81
+            </TouchableOpacity> }
62 82
             {isModerationSupported && ((participantCount === 1 || !allModerators)) && <>
63 83
                 {/* @ts-ignore */}
64 84
                 <Divider style = { styles.divider } />

+ 4
- 0
react/features/participants-pane/components/web/FooterContextMenu.tsx View File

@@ -23,6 +23,7 @@ import {
23 23
 import { MEDIA_TYPE } from '../../../base/media/constants';
24 24
 import {
25 25
     getParticipantCount,
26
+    getRaiseHandsQueue,
26 27
     isEveryoneModerator
27 28
 } from '../../../base/participants/functions';
28 29
 import { withPixelLineHeight } from '../../../base/styles/functions.web';
@@ -32,6 +33,7 @@ import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
32 33
 import { openSettingsDialog } from '../../../settings/actions.web';
33 34
 import { SETTINGS_TABS } from '../../../settings/constants';
34 35
 import { shouldShowModeratorSettings } from '../../../settings/functions.web';
36
+import LowerHandButton from '../../../video-menu/components/web/LowerHandButton';
35 37
 import MuteEveryonesVideoDialog from '../../../video-menu/components/web/MuteEveryonesVideoDialog';
36 38
 
37 39
 const useStyles = makeStyles()(theme => {
@@ -85,6 +87,7 @@ interface IProps {
85 87
 export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: IProps) => {
86 88
     const dispatch = useDispatch();
87 89
     const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state));
90
+    const raisedHandsQueue = useSelector(getRaiseHandsQueue);
88 91
     const allModerators = useSelector(isEveryoneModerator);
89 92
     const isModeratorSettingsTabEnabled = useSelector(shouldShowModeratorSettings);
90 93
     const participantCount = useSelector(getParticipantCount);
@@ -147,6 +150,7 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: IProp
147 150
                     onClick: muteAllVideo,
148 151
                     text: t('participantsPane.actions.stopEveryonesVideo')
149 152
                 } ] } />
153
+            {raisedHandsQueue.length !== 0 && <LowerHandButton />}
150 154
             {!isBreakoutRoom && isModerationSupported && (participantCount === 1 || !allModerators) && (
151 155
                 <ContextMenuItemGroup actions = { actions }>
152 156
                     <div className = { classes.text }>

+ 71
- 0
react/features/video-menu/components/native/LowerHandButton.tsx View File

@@ -0,0 +1,71 @@
1
+import { connect } from 'react-redux';
2
+
3
+import { IReduxState } from '../../../app/types';
4
+import { getCurrentConference } from '../../../base/conference/functions';
5
+import { IJitsiConference } from '../../../base/conference/reducer';
6
+import { translate } from '../../../base/i18n/functions';
7
+import { IconRaiseHand } from '../../../base/icons/svg';
8
+import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
9
+import { LOWER_HAND_MESSAGE } from '../../../base/tracks/constants';
10
+
11
+interface IProps extends AbstractButtonProps {
12
+
13
+    /**
14
+     * The current conference.
15
+     */
16
+    _conference: IJitsiConference | undefined;
17
+
18
+    /**
19
+     * The ID of the participant object that this button is supposed to
20
+     * ask to lower the hand.
21
+     */
22
+    participantId: String | undefined;
23
+}
24
+
25
+/**
26
+ * Implements a React {@link Component} which displays a button for lowering certain
27
+ * participant raised hands.
28
+ *
29
+ * @returns {JSX.Element}
30
+ */
31
+class LowerHandButton extends AbstractButton<IProps> {
32
+    icon = IconRaiseHand;
33
+    accessibilityLabel = 'participantsPane.actions.lowerHand';
34
+    label = 'participantsPane.actions.lowerHand';
35
+
36
+    /**
37
+     * Handles clicking / pressing the button, and asks the participant to lower hand.
38
+     *
39
+     * @private
40
+     * @returns {void}
41
+     */
42
+    _handleClick() {
43
+        const { participantId, _conference } = this.props;
44
+
45
+        _conference?.sendEndpointMessage(
46
+            participantId,
47
+            {
48
+                name: LOWER_HAND_MESSAGE
49
+            }
50
+        );
51
+    }
52
+}
53
+
54
+/**
55
+ * Maps part of the Redux state to the props of this component.
56
+ *
57
+ * @param {Object} state - The Redux state.
58
+ * @param {Object} ownProps - Properties of component.
59
+ * @returns {IProps}
60
+ */
61
+function mapStateToProps(state: IReduxState, ownProps: any) {
62
+    const { participantID } = ownProps;
63
+    const currentConference = getCurrentConference(state);
64
+
65
+    return {
66
+        _conference: currentConference,
67
+        participantId: participantID
68
+    };
69
+}
70
+
71
+export default translate(connect(mapStateToProps)(LowerHandButton));

+ 11
- 0
react/features/video-menu/components/native/RemoteVideoMenu.tsx View File

@@ -16,6 +16,7 @@ import { translate } from '../../../base/i18n/functions';
16 16
 import {
17 17
     getParticipantById,
18 18
     getParticipantDisplayName,
19
+    hasRaisedHand,
19 20
     isLocalParticipantModerator
20 21
 } from '../../../base/participants/functions';
21 22
 import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
@@ -27,6 +28,7 @@ import ConnectionStatusButton from './ConnectionStatusButton';
27 28
 import DemoteToVisitorButton from './DemoteToVisitorButton';
28 29
 import GrantModeratorButton from './GrantModeratorButton';
29 30
 import KickButton from './KickButton';
31
+import LowerHandButton from './LowerHandButton';
30 32
 import MuteButton from './MuteButton';
31 33
 import MuteEveryoneElseButton from './MuteEveryoneElseButton';
32 34
 import MuteVideoButton from './MuteVideoButton';
@@ -93,6 +95,11 @@ interface IProps {
93 95
      */
94 96
     _participantDisplayName: string;
95 97
 
98
+    /**
99
+     * Whether the targeted participant raised hand or not.
100
+     */
101
+    _raisedHand: boolean;
102
+
96 103
     /**
97 104
      * Array containing the breakout rooms.
98 105
      */
@@ -150,6 +157,7 @@ class RemoteVideoMenu extends PureComponent<IProps> {
150 157
             _isParticipantAvailable,
151 158
             _isParticipantSilent,
152 159
             _moderator,
160
+            _raisedHand,
153 161
             _rooms,
154 162
             _showDemote,
155 163
             _currentRoomId,
@@ -175,6 +183,7 @@ class RemoteVideoMenu extends PureComponent<IProps> {
175 183
                 {!_isParticipantSilent && <AskUnmuteButton { ...buttonProps } />}
176 184
                 { !_disableRemoteMute && <MuteButton { ...buttonProps } /> }
177 185
                 <MuteEveryoneElseButton { ...buttonProps } />
186
+                { _moderator && _raisedHand && <LowerHandButton { ...buttonProps } /> }
178 187
                 { !_disableRemoteMute && !_isParticipantSilent && <MuteVideoButton { ...buttonProps } /> }
179 188
                 {/* @ts-ignore */}
180 189
                 <Divider style = { styles.divider as ViewStyle } />
@@ -256,6 +265,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
256 265
     const moderator = isLocalParticipantModerator(state);
257 266
     const _iAmVisitor = state['features/visitors'].iAmVisitor;
258 267
     const _isBreakoutRoom = isInBreakoutRoom(state);
268
+    const raisedHand = hasRaisedHand(participant);
259 269
 
260 270
     return {
261 271
         _currentRoomId,
@@ -267,6 +277,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
267 277
         _isParticipantSilent: Boolean(participant?.isSilent),
268 278
         _moderator: moderator,
269 279
         _participantDisplayName: getParticipantDisplayName(state, participantId),
280
+        _raisedHand: raisedHand,
270 281
         _rooms,
271 282
         _showDemote: moderator
272 283
     };

+ 56
- 0
react/features/video-menu/components/web/LowerHandButton.tsx View File

@@ -0,0 +1,56 @@
1
+import React, { useCallback } from 'react';
2
+import { useTranslation } from 'react-i18next';
3
+import { useDispatch, useSelector } from 'react-redux';
4
+
5
+import { getCurrentConference } from '../../../base/conference/functions';
6
+import { IconRaiseHand } from '../../../base/icons/svg';
7
+import { raiseHand } from '../../../base/participants/actions';
8
+import { LOWER_HAND_MESSAGE } from '../../../base/tracks/constants';
9
+import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
10
+
11
+interface IProps {
12
+
13
+    /**
14
+     * The ID of the participant that's linked to the button.
15
+     */
16
+    participantID?: String;
17
+}
18
+
19
+/**
20
+ * Implements a React {@link Component} which displays a button for notifying certain
21
+ * participant who raised hand to lower hand.
22
+ *
23
+ * @returns {JSX.Element}
24
+ */
25
+const LowerHandButton = ({
26
+    participantID = ''
27
+}: IProps): JSX.Element => {
28
+    const { t } = useTranslation();
29
+    const dispatch = useDispatch();
30
+    const currentConference = useSelector(getCurrentConference);
31
+    const accessibilityText = participantID
32
+        ? t('participantsPane.actions.lowerHand')
33
+        : t('participantsPane.actions.lowerAllHands');
34
+
35
+    const handleClick = useCallback(() => {
36
+        if (!participantID) {
37
+            dispatch(raiseHand(false));
38
+        }
39
+        currentConference?.sendEndpointMessage(
40
+            participantID,
41
+            {
42
+                name: LOWER_HAND_MESSAGE
43
+            }
44
+        );
45
+    }, [ participantID ]);
46
+
47
+    return (
48
+        <ContextMenuItem
49
+            accessibilityLabel = { accessibilityText }
50
+            icon = { IconRaiseHand }
51
+            onClick = { handleClick }
52
+            text = { accessibilityText } />
53
+    );
54
+};
55
+
56
+export default LowerHandButton;

+ 7
- 1
react/features/video-menu/components/web/ParticipantContextMenu.tsx View File

@@ -9,7 +9,7 @@ import Avatar from '../../../base/avatar/components/Avatar';
9 9
 import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/utils';
10 10
 import { MEDIA_TYPE } from '../../../base/media/constants';
11 11
 import { PARTICIPANT_ROLE } from '../../../base/participants/constants';
12
-import { getLocalParticipant } from '../../../base/participants/functions';
12
+import { getLocalParticipant, hasRaisedHand } from '../../../base/participants/functions';
13 13
 import { IParticipant } from '../../../base/participants/types';
14 14
 import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks/functions.any';
15 15
 import ContextMenu from '../../../base/ui/components/web/ContextMenu';
@@ -33,6 +33,7 @@ import CustomOptionButton from './CustomOptionButton';
33 33
 import DemoteToVisitorButton from './DemoteToVisitorButton';
34 34
 import GrantModeratorButton from './GrantModeratorButton';
35 35
 import KickButton from './KickButton';
36
+import LowerHandButton from './LowerHandButton';
36 37
 import MuteButton from './MuteButton';
37 38
 import MuteEveryoneElseButton from './MuteEveryoneElseButton';
38 39
 import MuteEveryoneElsesVideoButton from './MuteEveryoneElsesVideoButton';
@@ -148,6 +149,7 @@ const ParticipantContextMenu = ({
148 149
         : participant?.id ? participantsVolume[participant?.id] : undefined) ?? 1;
149 150
     const isBreakoutRoom = useSelector(isInBreakoutRoom);
150 151
     const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state));
152
+    const raisedHands = hasRaisedHand(participant);
151 153
     const stageFilmstrip = useSelector(isStageFilmstripAvailable);
152 154
     const shouldDisplayVerification = useSelector((state: IReduxState) => displayVerification(state, participant?.id));
153 155
     const buttonsWithNotifyClick = useSelector(getParticipantMenuButtonsWithNotifyClick);
@@ -241,6 +243,10 @@ const ParticipantContextMenu = ({
241 243
             buttons.push(<MuteEveryoneElsesVideoButton { ...getButtonProps(BUTTONS.MUTE_OTHERS_VIDEO) } />);
242 244
         }
243 245
 
246
+        if (raisedHands) {
247
+            buttons2.push(<LowerHandButton { ...getButtonProps(BUTTONS.LOWER_PARTICIPANT_HAND) } />);
248
+        }
249
+
244 250
         if (!disableGrantModerator && !isBreakoutRoom) {
245 251
             buttons2.push(<GrantModeratorButton { ...getButtonProps(BUTTONS.GRANT_MODERATOR) } />);
246 252
         }

+ 1
- 0
react/features/video-menu/constants.ts View File

@@ -25,6 +25,7 @@ export const PARTICIPANT_MENU_BUTTONS = {
25 25
     GRANT_MODERATOR: 'grant-moderator',
26 26
     HIDE_SELF_VIEW: 'hide-self-view',
27 27
     KICK: 'kick',
28
+    LOWER_PARTICIPANT_HAND: 'lower-participant-hand',
28 29
     MUTE: 'mute',
29 30
     MUTE_OTHERS: 'mute-others',
30 31
     MUTE_OTHERS_VIDEO: 'mute-others-video',

Loading…
Cancel
Save