Browse Source

feat: add grant moderator functionality

master^2
Gabriel Imre 4 years ago
parent
commit
b85cd2348f
No account linked to committer's email address

+ 4
- 0
lang/main.json View File

@@ -203,6 +203,8 @@
203 203
         "enterDisplayName": "Please enter your name here",
204 204
         "error": "Error",
205 205
         "gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
206
+        "grantModeratorDialog": "Are you sure you want to make this participant a moderator?",
207
+        "grantModeratorTitle": "Grant moderator",
206 208
         "IamHost": "I am the host",
207 209
         "incorrectRoomLockPassword": "Incorrect password",
208 210
         "incorrectPassword": "Incorrect username or password",
@@ -669,6 +671,7 @@
669 671
             "e2ee": "End-to-End Encryption",
670 672
             "feedback": "Leave feedback",
671 673
             "fullScreen": "Toggle full screen",
674
+            "grantModerator": "Grant Moderator",
672 675
             "hangup": "Leave the call",
673 676
             "help": "Help",
674 677
             "invite": "Invite people",
@@ -817,6 +820,7 @@
817 820
         "domute": "Mute",
818 821
         "domuteOthers": "Mute everyone else",
819 822
         "flip": "Flip",
823
+        "grantModerator": "Grant Moderator",
820 824
         "kick": "Kick out",
821 825
         "moderator": "Moderator",
822 826
         "mute": "Participant is muted",

+ 3
- 0
react/features/base/icons/svg/crown.svg View File

@@ -0,0 +1,3 @@
1
+<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M14 4C14 4.85739 13.4605 5.58876 12.7024 5.87317L14.2286 9.94296L14.9455 11.8546C15.0074 11.9292 15.0708 11.9292 15.1098 11.8902L16.5535 10.4465L18.5858 8.41421C18.2239 8.05228 18 7.55228 18 7C18 5.89543 18.8954 5 20 5C21.1046 5 22 5.89543 22 7C22 8.10457 21.1046 9 20 9C19.9441 9 19.8887 8.9977 19.8339 8.9932L19 19C19 20.1046 18.1046 21 17 21H7C5.89543 21 5 20.1046 5 19L4.1661 8.9932C4.11133 8.9977 4.05593 9 4 9C2.89543 9 2 8.10457 2 7C2 5.89543 2.89543 5 4 5C5.10457 5 6 5.89543 6 7C6 7.55228 5.77614 8.05228 5.41421 8.41421L7.44654 10.4465L8.89019 11.8902C8.9775 11.9325 9.03514 11.9063 9.05453 11.8546L9.77139 9.94296L11.2976 5.87317C10.5395 5.58876 10 4.85739 10 4C10 2.89543 10.8954 2 12 2C13.1046 2 14 2.89543 14 4ZM6.84027 17L6.44651 12.2749L7.47597 13.3044C7.68795 13.5164 7.94285 13.6805 8.22354 13.7858C9.30949 14.193 10.52 13.6428 10.9272 12.5568L12 9.696L13.0728 12.5568C13.1781 12.8375 13.3422 13.0924 13.5542 13.3044C14.3743 14.1245 15.7039 14.1245 16.524 13.3044L17.5535 12.2749L17.1597 17H6.84027Z"/>
3
+</svg>

+ 1
- 0
react/features/base/icons/svg/index.js View File

@@ -23,6 +23,7 @@ export { default as IconClosedCaption } from './closed_caption.svg';
23 23
 export { default as IconConnectionActive } from './gsm-bars.svg';
24 24
 export { default as IconConnectionInactive } from './ninja.svg';
25 25
 export { default as IconCopy } from './copy.svg';
26
+export { default as IconCrown } from './crown.svg';
26 27
 export { default as IconDeviceBluetooth } from './bluetooth.svg';
27 28
 export { default as IconDeviceEarpiece } from './phone-talk.svg';
28 29
 export { default as IconDeviceHeadphone } from './headset.svg';

+ 10
- 0
react/features/base/participants/actionTypes.js View File

@@ -12,6 +12,16 @@
12 12
  */
13 13
 export const DOMINANT_SPEAKER_CHANGED = 'DOMINANT_SPEAKER_CHANGED';
14 14
 
15
+/**
16
+ * Create an action for granting moderator to a participant.
17
+ *
18
+ * {
19
+ *     type: GRANT_MODERATOR,
20
+ *     id: string
21
+ * }
22
+ */
23
+export const GRANT_MODERATOR = 'GRANT_MODERATOR';
24
+
15 25
 /**
16 26
  * Create an action for removing a participant from the conference.
17 27
  *

+ 17
- 0
react/features/base/participants/actions.js View File

@@ -5,6 +5,7 @@ import {
5 5
     DOMINANT_SPEAKER_CHANGED,
6 6
     HIDDEN_PARTICIPANT_JOINED,
7 7
     HIDDEN_PARTICIPANT_LEFT,
8
+    GRANT_MODERATOR,
8 9
     KICK_PARTICIPANT,
9 10
     MUTE_REMOTE_PARTICIPANT,
10 11
     PARTICIPANT_ID_CHANGED,
@@ -47,6 +48,22 @@ export function dominantSpeakerChanged(id, conference) {
47 48
     };
48 49
 }
49 50
 
51
+/**
52
+ * Create an action for granting moderator to a participant.
53
+ *
54
+ * @param {string} id - Participant's ID.
55
+ * @returns {{
56
+ *     type: GRANT_MODERATOR,
57
+ *     id: string
58
+ * }}
59
+ */
60
+export function grantModerator(id) {
61
+    return {
62
+        type: GRANT_MODERATOR,
63
+        id
64
+    };
65
+}
66
+
50 67
 /**
51 68
  * Create an action for removing a participant from the conference.
52 69
  *

+ 11
- 7
react/features/base/participants/functions.js View File

@@ -259,6 +259,16 @@ export function getYoutubeParticipant(stateful: Object | Function) {
259 259
     return participants.filter(p => p.isFakeParticipant)[0];
260 260
 }
261 261
 
262
+/**
263
+ * Returns true if the participant is a moderator.
264
+ *
265
+ * @param {string} participant - Participant object.
266
+ * @returns {boolean}
267
+ */
268
+export function isParticipantModerator(participant: Object) {
269
+    return participant?.role === PARTICIPANT_ROLE.MODERATOR;
270
+}
271
+
262 272
 /**
263 273
  * Returns true if all of the meeting participants are moderators.
264 274
  *
@@ -269,13 +279,7 @@ export function getYoutubeParticipant(stateful: Object | Function) {
269 279
 export function isEveryoneModerator(stateful: Object | Function) {
270 280
     const participants = _getAllParticipants(stateful);
271 281
 
272
-    for (const participant of participants) {
273
-        if (participant.role !== PARTICIPANT_ROLE.MODERATOR) {
274
-            return false;
275
-        }
276
-    }
277
-
278
-    return true;
282
+    return participants.every(isParticipantModerator);
279 283
 }
280 284
 
281 285
 /**

+ 8
- 0
react/features/base/participants/middleware.js View File

@@ -15,6 +15,7 @@ import { playSound, registerSound, unregisterSound } from '../sounds';
15 15
 
16 16
 import {
17 17
     DOMINANT_SPEAKER_CHANGED,
18
+    GRANT_MODERATOR,
18 19
     KICK_PARTICIPANT,
19 20
     MUTE_REMOTE_PARTICIPANT,
20 21
     PARTICIPANT_DISPLAY_NAME_CHANGED,
@@ -86,6 +87,13 @@ MiddlewareRegistry.register(store => next => action => {
86 87
         break;
87 88
     }
88 89
 
90
+    case GRANT_MODERATOR: {
91
+        const { conference } = store.getState()['features/base/conference'];
92
+
93
+        conference.grantOwner(action.id);
94
+        break;
95
+    }
96
+
89 97
     case KICK_PARTICIPANT: {
90 98
         const { conference } = store.getState()['features/base/conference'];
91 99
 

+ 10
- 1
react/features/base/testing/components/TestConnectionInfo.js View File

@@ -36,6 +36,11 @@ type Props = {
36 36
      */
37 37
     _localUserId: string,
38 38
 
39
+    /**
40
+     * The local participant's role.
41
+     */
42
+    _localUserRole: string,
43
+
39 44
     /**
40 45
      * Indicates whether or not the test mode is currently on. Otherwise the
41 46
      * TestConnectionInfo component will not render.
@@ -179,6 +184,9 @@ class TestConnectionInfo extends Component<Props, State> {
179 184
                 <TestHint
180 185
                     id = 'org.jitsi.meet.conference.joinedState'
181 186
                     value = { this.props._conferenceJoinedState } />
187
+                <TestHint
188
+                    id = 'org.jitsi.meet.conference.localParticipantRole'
189
+                    value = { this.props._localUserRole } />
182 190
                 <TestHint
183 191
                     id = 'org.jitsi.meet.stats.rtp'
184 192
                     value = { JSON.stringify(this.state.stats) } />
@@ -208,7 +216,8 @@ function _mapStateToProps(state) {
208 216
     return {
209 217
         _conferenceConnectionState: state['features/testing'].connectionState,
210 218
         _conferenceJoinedState: conferenceJoined.toString(),
211
-        _localUserId: localParticipant && localParticipant.id,
219
+        _localUserId: localParticipant?.id,
220
+        _localUserRole: localParticipant?.role,
212 221
         _testMode: isTestModeEnabled(state)
213 222
     };
214 223
 }

+ 70
- 0
react/features/remote-video-menu/components/AbstractGrantModeratorButton.js View File

@@ -0,0 +1,70 @@
1
+// @flow
2
+
3
+import { openDialog } from '../../base/dialog';
4
+import { IconCrown } from '../../base/icons';
5
+import {
6
+    getParticipantById,
7
+    isLocalParticipantModerator,
8
+    isParticipantModerator
9
+} from '../../base/participants';
10
+import { AbstractButton } from '../../base/toolbox';
11
+import type { AbstractButtonProps } from '../../base/toolbox';
12
+
13
+import { GrantModeratorDialog } from '.';
14
+
15
+export type Props = AbstractButtonProps & {
16
+
17
+    /**
18
+     * The redux {@code dispatch} function.
19
+     */
20
+    dispatch: Function,
21
+
22
+    /**
23
+     * The ID of the participant for whom to grant moderator status.
24
+     */
25
+    participantID: string,
26
+
27
+    /**
28
+     * The function to be used to translate i18n labels.
29
+     */
30
+    t: Function
31
+};
32
+
33
+/**
34
+ * An abstract remote video menu button which kicks the remote participant.
35
+ */
36
+export default class AbstractGrantModeratorButton extends AbstractButton<Props, *> {
37
+  accessibilityLabel = 'toolbar.accessibilityLabel.grantModerator';
38
+  icon = IconCrown;
39
+  label = 'videothumbnail.grantModerator';
40
+
41
+  /**
42
+   * Handles clicking / pressing the button, and kicks the participant.
43
+   *
44
+   * @private
45
+   * @returns {void}
46
+   */
47
+  _handleClick() {
48
+      const { dispatch, participantID } = this.props;
49
+
50
+      dispatch(openDialog(GrantModeratorDialog, { participantID }));
51
+  }
52
+}
53
+
54
+/**
55
+ * Function that maps parts of Redux state tree into component props.
56
+ *
57
+ * @param {Object} state - Redux state.
58
+ * @param {Object} ownProps - Properties of component.
59
+ * @private
60
+ * @returns {{
61
+ *     visible: boolean
62
+ * }}
63
+ */
64
+export function _mapStateToProps(state: Object, ownProps: Props) {
65
+    const { participantID } = ownProps;
66
+
67
+    return {
68
+        visible: isLocalParticipantModerator(state) && !isParticipantModerator(getParticipantById(state, participantID))
69
+    };
70
+}

+ 66
- 0
react/features/remote-video-menu/components/AbstractGrantModeratorDialog.js View File

@@ -0,0 +1,66 @@
1
+// @flow
2
+
3
+import { Component } from 'react';
4
+
5
+import {
6
+    createRemoteVideoMenuButtonEvent,
7
+    sendAnalytics
8
+} from '../../analytics';
9
+import { grantModerator } from '../../base/participants';
10
+
11
+type Props = {
12
+
13
+    /**
14
+     * The Redux dispatch function.
15
+     */
16
+    dispatch: Function,
17
+
18
+    /**
19
+     * The ID of the remote participant to be granted moderator rights.
20
+     */
21
+    participantID: string,
22
+
23
+    /**
24
+     * Function to translate i18n labels.
25
+     */
26
+    t: Function
27
+};
28
+
29
+/**
30
+ * Abstract dialog to confirm granting moderator to a participant.
31
+ */
32
+export default class AbstractGrantModeratorDialog
33
+    extends Component<Props> {
34
+    /**
35
+     * Initializes a new {@code AbstractGrantModeratorDialog} instance.
36
+     *
37
+     * @inheritdoc
38
+     */
39
+    constructor(props: Props) {
40
+        super(props);
41
+
42
+        this._onSubmit = this._onSubmit.bind(this);
43
+    }
44
+
45
+    _onSubmit: () => boolean;
46
+
47
+    /**
48
+     * Callback for the confirm button.
49
+     *
50
+     * @private
51
+     * @returns {boolean} - True (to note that the modal should be closed).
52
+     */
53
+    _onSubmit() {
54
+        const { dispatch, participantID } = this.props;
55
+
56
+        sendAnalytics(createRemoteVideoMenuButtonEvent(
57
+            'grant.moderator.button',
58
+            {
59
+                'participant_id': participantID
60
+            }));
61
+
62
+        dispatch(grantModerator(participantID));
63
+
64
+        return true;
65
+    }
66
+}

+ 9
- 0
react/features/remote-video-menu/components/native/GrantModeratorButton.js View File

@@ -0,0 +1,9 @@
1
+// @flow
2
+
3
+import { translate } from '../../../base/i18n';
4
+import { connect } from '../../../base/redux';
5
+import AbstractGrantModeratorButton, {
6
+    _mapStateToProps
7
+} from '../AbstractGrantModeratorButton';
8
+
9
+export default translate(connect(_mapStateToProps)(AbstractGrantModeratorButton));

+ 32
- 0
react/features/remote-video-menu/components/native/GrantModeratorDialog.js View File

@@ -0,0 +1,32 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { ConfirmDialog } from '../../../base/dialog';
6
+import { translate } from '../../../base/i18n';
7
+import { connect } from '../../../base/redux';
8
+import AbstractGrantModeratorDialog
9
+    from '../AbstractGrantModeratorDialog';
10
+
11
+/**
12
+ * Dialog to confirm a remote participant kick action.
13
+ */
14
+class GrantModeratorDialog extends AbstractGrantModeratorDialog {
15
+    /**
16
+     * Implements React's {@link Component#render()}.
17
+     *
18
+     * @inheritdoc
19
+     * @returns {ReactElement}
20
+     */
21
+    render() {
22
+        return (
23
+            <ConfirmDialog
24
+                contentKey = 'dialog.grantModeratorDialog'
25
+                onSubmit = { this._onSubmit } />
26
+        );
27
+    }
28
+
29
+    _onSubmit: () => boolean;
30
+}
31
+
32
+export default translate(connect()(GrantModeratorDialog));

+ 3
- 0
react/features/remote-video-menu/components/native/RemoteVideoMenu.js View File

@@ -12,6 +12,7 @@ import { StyleType } from '../../../base/styles';
12 12
 import { PrivateMessageButton } from '../../../chat';
13 13
 import { hideRemoteVideoMenu } from '../../actions';
14 14
 
15
+import GrantModeratorButton from './GrantModeratorButton';
15 16
 import KickButton from './KickButton';
16 17
 import MuteButton from './MuteButton';
17 18
 import PinButton from './PinButton';
@@ -98,6 +99,8 @@ class RemoteVideoMenu extends Component<Props> {
98 99
             buttons.push(<MuteButton { ...buttonProps } />);
99 100
         }
100 101
 
102
+        buttons.push(<GrantModeratorButton { ...buttonProps } />);
103
+
101 104
         if (!_disableKick) {
102 105
             buttons.push(<KickButton { ...buttonProps } />);
103 106
         }

+ 3
- 0
react/features/remote-video-menu/components/native/index.js View File

@@ -1,5 +1,8 @@
1 1
 // @flow
2 2
 
3
+export {
4
+    default as GrantModeratorDialog
5
+} from './GrantModeratorDialog';
3 6
 export {
4 7
     default as KickRemoteParticipantDialog
5 8
 } from './KickRemoteParticipantDialog';

+ 60
- 0
react/features/remote-video-menu/components/web/GrantModeratorButton.js View File

@@ -0,0 +1,60 @@
1
+/* @flow */
2
+
3
+import React from 'react';
4
+
5
+import { translate } from '../../../base/i18n';
6
+import { IconCrown } from '../../../base/icons';
7
+import { connect } from '../../../base/redux';
8
+import AbstractGrantModeratorButton, {
9
+    _mapStateToProps,
10
+    type Props
11
+} from '../AbstractGrantModeratorButton';
12
+
13
+import RemoteVideoMenuButton from './RemoteVideoMenuButton';
14
+
15
+declare var interfaceConfig: Object;
16
+
17
+/**
18
+ * Implements a React {@link Component} which displays a button for granting
19
+ * moderator to a participant.
20
+ */
21
+class GrantModeratorButton extends AbstractGrantModeratorButton {
22
+    /**
23
+     * Instantiates a new {@code GrantModeratorButton}.
24
+     *
25
+     * @inheritdoc
26
+     */
27
+    constructor(props: Props) {
28
+        super(props);
29
+
30
+        this._handleClick = this._handleClick.bind(this);
31
+    }
32
+
33
+    /**
34
+     * Implements React's {@link Component#render()}.
35
+     *
36
+     * @inheritdoc
37
+     * @returns {ReactElement}
38
+     */
39
+    render() {
40
+        const { participantID, t, visible } = this.props;
41
+
42
+        if (!visible) {
43
+            return null;
44
+        }
45
+
46
+        return (
47
+            <RemoteVideoMenuButton
48
+                buttonText = { t('videothumbnail.grantModerator') }
49
+                displayClass = 'grantmoderatorlink'
50
+                icon = { IconCrown }
51
+                id = { `grantmoderatorlink_${participantID}` }
52
+                // eslint-disable-next-line react/jsx-handler-names
53
+                onClick = { this._handleClick } />
54
+        );
55
+    }
56
+
57
+    _handleClick: () => void
58
+}
59
+
60
+export default translate(connect(_mapStateToProps)(GrantModeratorButton));

+ 38
- 0
react/features/remote-video-menu/components/web/GrantModeratorDialog.js View File

@@ -0,0 +1,38 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { Dialog } from '../../../base/dialog';
6
+import { translate } from '../../../base/i18n';
7
+import { connect } from '../../../base/redux';
8
+import AbstractGrantModeratorDialog
9
+    from '../AbstractGrantModeratorDialog';
10
+
11
+/**
12
+ * Dialog to confirm a grant moderator action.
13
+ */
14
+class GrantModeratorDialog extends AbstractGrantModeratorDialog {
15
+    /**
16
+     * Implements React's {@link Component#render()}.
17
+     *
18
+     * @inheritdoc
19
+     * @returns {ReactElement}
20
+     */
21
+    render() {
22
+        return (
23
+            <Dialog
24
+                okKey = 'dialog.Yes'
25
+                onSubmit = { this._onSubmit }
26
+                titleKey = 'dialog.grantModeratorTitle'
27
+                width = 'small'>
28
+                <div>
29
+                    { this.props.t('dialog.grantModeratorDialog') }
30
+                </div>
31
+            </Dialog>
32
+        );
33
+    }
34
+
35
+    _onSubmit: () => boolean;
36
+}
37
+
38
+export default translate(connect()(GrantModeratorDialog));

+ 7
- 0
react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js View File

@@ -8,6 +8,7 @@ import { Popover } from '../../../base/popover';
8 8
 import { connect } from '../../../base/redux';
9 9
 
10 10
 import {
11
+    GrantModeratorButton,
11 12
     MuteButton,
12 13
     MuteEveryoneElseButton,
13 14
     KickButton,
@@ -195,6 +196,12 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
195 196
                 );
196 197
             }
197 198
 
199
+            buttons.push(
200
+                <GrantModeratorButton
201
+                    key = 'grant-moderator'
202
+                    participantID = { participantID } />
203
+            );
204
+
198 205
             if (!_disableKick) {
199 206
                 buttons.push(
200 207
                     <KickButton

+ 4
- 0
react/features/remote-video-menu/components/web/index.js View File

@@ -1,5 +1,9 @@
1 1
 // @flow
2 2
 
3
+export { default as GrantModeratorButton } from './GrantModeratorButton';
4
+export {
5
+    default as GrantModeratorDialog
6
+} from './GrantModeratorDialog';
3 7
 export { default as KickButton } from './KickButton';
4 8
 export {
5 9
     default as KickRemoteParticipantDialog

Loading…
Cancel
Save