Browse Source

feat(rn,av-moderation) updated advanced moderation on Native

Updated participants list to:
- show Moderator label
- show correct status icons (red for force muted)
- show participants in the right order

Updated moderation to:
- show moderation menu at all times
- make moderation options functional

Updated notifications:
- fixed raise hand to show name
- display moderator rights granted

Updated mute/ stop video for all dialogs to include moderation toggles

Added ask to unmute button

Fix comments on ask to unmute

Co-authored-by: robertpin <robert.pin9@gmail.com>
master
robertpin 4 years ago
parent
commit
c3dae1f6e9
No account linked to committer's email address

+ 1
- 0
react/features/app/middlewares.any.js View File

@@ -1,6 +1,7 @@
1 1
 // @flow
2 2
 
3 3
 import '../analytics/middleware';
4
+import '../av-moderation/middleware';
4 5
 import '../base/conference/middleware';
5 6
 import '../base/config/middleware';
6 7
 import '../base/jwt/middleware';

+ 0
- 1
react/features/app/middlewares.web.js View File

@@ -1,7 +1,6 @@
1 1
 // @flow
2 2
 
3 3
 import '../authentication/middleware';
4
-import '../av-moderation/middleware';
5 4
 import '../base/devices/middleware';
6 5
 import '../e2ee/middleware';
7 6
 import '../external-api/middleware';

+ 1
- 0
react/features/app/reducers.any.js View File

@@ -2,6 +2,7 @@
2 2
 
3 3
 import '../analytics/reducer';
4 4
 import '../authentication/reducer';
5
+import '../av-moderation/reducer';
5 6
 import '../base/app/reducer';
6 7
 import '../base/audio-only/reducer';
7 8
 import '../base/color-scheme/reducer';

+ 0
- 1
react/features/app/reducers.web.js View File

@@ -1,6 +1,5 @@
1 1
 // @flow
2 2
 
3
-import '../av-moderation/reducer';
4 3
 import '../base/devices/reducer';
5 4
 import '../e2ee/reducer';
6 5
 import '../feedback/reducer';

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

@@ -546,6 +546,7 @@ function _raiseHandUpdated({ dispatch, getState }, conference, participantId, ne
546 546
             title: getParticipantDisplayName(state, participantId),
547 547
             descriptionKey: 'notify.raisedHand',
548 548
             raiseHandNotification: true,
549
+            concatText: true,
549 550
             ...action
550 551
         }, NOTIFICATION_TIMEOUT * (shouldDisplayAllowAction ? 2 : 1)));
551 552
         dispatch(playSound(RAISE_HAND_SOUND_ID));

+ 3
- 1
react/features/filmstrip/components/native/Thumbnail.js View File

@@ -217,7 +217,9 @@ class Thumbnail extends PureComponent<Props> {
217 217
         const indicators = [];
218 218
 
219 219
         if (renderModeratorIndicator) {
220
-            indicators.push(<View style = { styles.moderatorIndicatorContainer }>
220
+            indicators.push(<View
221
+                key = 'moderator-indicator'
222
+                style = { styles.moderatorIndicatorContainer }>
221 223
                 <ModeratorIndicator />
222 224
             </View>);
223 225
         }

+ 5
- 0
react/features/notifications/components/AbstractNotification.js View File

@@ -12,6 +12,11 @@ export type Props = {
12 12
      */
13 13
     appearance: string,
14 14
 
15
+    /**
16
+     * Whether or not the title and description should be concatenated.
17
+     */
18
+    concatText?: boolean,
19
+
15 20
     /**
16 21
      * Callback invoked when the custom button is clicked.
17 22
      */

+ 7
- 2
react/features/notifications/components/native/Notification.js View File

@@ -66,12 +66,17 @@ class Notification extends AbstractNotification<Props> {
66 66
      * @private
67 67
      */
68 68
     _renderContent() {
69
-        const { maxLines = DEFAULT_MAX_LINES, t, title, titleArguments, titleKey } = this.props;
69
+        const { maxLines = DEFAULT_MAX_LINES, t, title, titleArguments, titleKey, concatText } = this.props;
70 70
         const titleText = title || (titleKey && t(titleKey, titleArguments));
71 71
         const description = this._getDescription();
72
+        const titleConcat = [];
73
+
74
+        if (concatText) {
75
+            titleConcat.push(titleText);
76
+        }
72 77
 
73 78
         if (description && description.length) {
74
-            return description.map((line, index) => (
79
+            return [ ...titleConcat, ...description ].map((line, index) => (
75 80
                 <Text
76 81
                     key = { index }
77 82
                     numberOfLines = { maxLines }

+ 72
- 13
react/features/participants-pane/components/native/ContextMenuMore.js View File

@@ -2,20 +2,29 @@
2 2
 
3 3
 import React, { useCallback } from 'react';
4 4
 import { useTranslation } from 'react-i18next';
5
-import { TouchableOpacity } from 'react-native';
6
-import { Text } from 'react-native-paper';
5
+import { TouchableOpacity, View } from 'react-native';
6
+import { Divider, Text } from 'react-native-paper';
7 7
 import { useDispatch, useSelector } from 'react-redux';
8 8
 
9
+import {
10
+    requestDisableAudioModeration,
11
+    requestDisableVideoModeration,
12
+    requestEnableAudioModeration,
13
+    requestEnableVideoModeration
14
+} from '../../../av-moderation/actions';
15
+import {
16
+    isSupported as isAvModerationSupported,
17
+    isEnabled as isAvModerationEnabled
18
+} from '../../../av-moderation/functions';
9 19
 import { openDialog, hideDialog } from '../../../base/dialog/actions';
10 20
 import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
11 21
 import {
12 22
     Icon,
23
+    IconCheck,
13 24
     IconVideoOff
14 25
 } from '../../../base/icons';
15
-import {
16
-    getLocalParticipant,
17
-    getParticipantCount
18
-} from '../../../base/participants';
26
+import { MEDIA_TYPE } from '../../../base/media';
27
+import { getParticipantCount, isEveryoneModerator } from '../../../base/participants';
19 28
 import MuteEveryonesVideoDialog
20 29
     from '../../../video-menu/components/native/MuteEveryonesVideoDialog';
21 30
 
@@ -24,21 +33,29 @@ import styles from './styles';
24 33
 export const ContextMenuMore = () => {
25 34
     const dispatch = useDispatch();
26 35
     const cancel = useCallback(() => dispatch(hideDialog()), [ dispatch ]);
27
-    const { id } = useSelector(getLocalParticipant);
28
-    const participantsCount = useSelector(getParticipantCount);
29
-    const showSlidingView = participantsCount > 2;
30 36
     const muteAllVideo = useCallback(() =>
31
-        dispatch(openDialog(MuteEveryonesVideoDialog,
32
-            { exclude: [ id ] })),
37
+        dispatch(openDialog(MuteEveryonesVideoDialog)),
33 38
         [ dispatch ]);
34 39
     const { t } = useTranslation();
35 40
 
41
+    const isModerationSupported = useSelector(isAvModerationSupported());
42
+    const allModerators = useSelector(isEveryoneModerator);
43
+    const participantCount = useSelector(getParticipantCount);
44
+
45
+    const isAudioModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO));
46
+    const isVideoModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.VIDEO));
47
+
48
+    const disableAudioModeration = useCallback(() => dispatch(requestDisableAudioModeration()), [ dispatch ]);
49
+    const disableVideoModeration = useCallback(() => dispatch(requestDisableVideoModeration()), [ dispatch ]);
50
+
51
+    const enableAudioModeration = useCallback(() => dispatch(requestEnableAudioModeration()), [ dispatch ]);
52
+    const enableVideoModeration = useCallback(() => dispatch(requestEnableVideoModeration()), [ dispatch ]);
53
+
36 54
     return (
37 55
         <BottomSheet
38 56
             addScrollViewPadding = { false }
39 57
             onCancel = { cancel }
40
-            showSlidingView = { showSlidingView }
41
-            style = { styles.contextMenuMore }>
58
+            showSlidingView = { true }>
42 59
             <TouchableOpacity
43 60
                 onPress = { muteAllVideo }
44 61
                 style = { styles.contextMenuItem }>
@@ -47,6 +64,48 @@ export const ContextMenuMore = () => {
47 64
                     src = { IconVideoOff } />
48 65
                 <Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.stopEveryonesVideo')}</Text>
49 66
             </TouchableOpacity>
67
+            {isModerationSupported && ((participantCount === 1 || !allModerators)) && <>
68
+                <Divider style = { styles.divider } />
69
+                <View style = { styles.contextMenuItem }>
70
+                    <Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.allow')}</Text>
71
+                </View>
72
+                {isAudioModerationEnabled
73
+                    ? <TouchableOpacity
74
+                        onPress = { disableAudioModeration }
75
+                        style = { styles.contextMenuItem }>
76
+                        <Text style = { styles.contextMenuItemTextNoIcon }>
77
+                            {t('participantsPane.actions.audioModeration')}
78
+                        </Text>
79
+                    </TouchableOpacity>
80
+                    : <TouchableOpacity
81
+                        onPress = { enableAudioModeration }
82
+                        style = { styles.contextMenuItem }>
83
+                        <Icon
84
+                            size = { 24 }
85
+                            src = { IconCheck } />
86
+                        <Text style = { styles.contextMenuItemText }>
87
+                            {t('participantsPane.actions.audioModeration')}
88
+                        </Text>
89
+                    </TouchableOpacity> }
90
+                {isVideoModerationEnabled
91
+                    ? <TouchableOpacity
92
+                        onPress = { disableVideoModeration }
93
+                        style = { styles.contextMenuItem }>
94
+                        <Text style = { styles.contextMenuItemTextNoIcon }>
95
+                            {t('participantsPane.actions.videoModeration')}
96
+                        </Text>
97
+                    </TouchableOpacity>
98
+                    : <TouchableOpacity
99
+                        onPress = { enableVideoModeration }
100
+                        style = { styles.contextMenuItem }>
101
+                        <Icon
102
+                            size = { 24 }
103
+                            src = { IconCheck } />
104
+                        <Text style = { styles.contextMenuItemText }>
105
+                            {t('participantsPane.actions.videoModeration')}
106
+                        </Text>
107
+                    </TouchableOpacity>}
108
+            </>}
50 109
         </BottomSheet>
51 110
     );
52 111
 };

+ 10
- 0
react/features/participants-pane/components/native/HorizontalDotsIcon.js View File

@@ -0,0 +1,10 @@
1
+// @flow
2
+import React from 'react';
3
+
4
+import { Icon, IconHorizontalPoints } from '../../../base/icons';
5
+
6
+const HorizontalDotsIcon = () => (<Icon
7
+    size = { 20 }
8
+    src = { IconHorizontalPoints } />);
9
+
10
+export default HorizontalDotsIcon;

+ 20
- 11
react/features/participants-pane/components/native/MeetingParticipantItem.js View File

@@ -6,7 +6,8 @@ import { translate } from '../../../base/i18n';
6 6
 import {
7 7
     getLocalParticipant,
8 8
     getParticipantByIdOrUndefined,
9
-    getParticipantDisplayName
9
+    getParticipantDisplayName,
10
+    isParticipantModerator
10 11
 } from '../../../base/participants';
11 12
 import { connect } from '../../../base/redux';
12 13
 import {
@@ -14,9 +15,8 @@ import {
14 15
     isParticipantVideoMuted
15 16
 } from '../../../base/tracks';
16 17
 import { showConnectionStatus, showContextMenuDetails, showSharedVideoMenu } from '../../actions.native';
17
-import { MEDIA_STATE } from '../../constants';
18 18
 import type { MediaState } from '../../constants';
19
-import { getParticipantAudioMediaState } from '../../functions';
19
+import { getParticipantAudioMediaState, getParticipantVideoMediaState } from '../../functions';
20 20
 
21 21
 import ParticipantItem from './ParticipantItem';
22 22
 
@@ -39,9 +39,9 @@ type Props = {
39 39
     _isFakeParticipant: boolean,
40 40
 
41 41
     /**
42
-     * True if the participant is video muted.
42
+     * Whether or not the user is a moderator.
43 43
      */
44
-    _isVideoMuted: boolean,
44
+    _isModerator: boolean,
45 45
 
46 46
     /**
47 47
      * True if the participant is the local participant.
@@ -63,6 +63,11 @@ type Props = {
63 63
      */
64 64
     _raisedHand: boolean,
65 65
 
66
+    /**
67
+     * Media state for video.
68
+     */
69
+    _videoMediaState: MediaState,
70
+
66 71
     /**
67 72
      * The redux dispatch function.
68 73
      */
@@ -127,10 +132,11 @@ class MeetingParticipantItem extends PureComponent<Props> {
127 132
         const {
128 133
             _audioMediaState,
129 134
             _displayName,
130
-            _isVideoMuted,
135
+            _isModerator,
131 136
             _local,
132 137
             _participantID,
133
-            _raisedHand
138
+            _raisedHand,
139
+            _videoMediaState
134 140
         } = this.props;
135 141
 
136 142
         return (
@@ -138,11 +144,12 @@ class MeetingParticipantItem extends PureComponent<Props> {
138 144
                 audioMediaState = { _audioMediaState }
139 145
                 displayName = { _displayName }
140 146
                 isKnockingParticipant = { false }
147
+                isModerator = { _isModerator }
141 148
                 local = { _local }
142 149
                 onPress = { this._onPress }
143 150
                 participantID = { _participantID }
144 151
                 raisedHand = { _raisedHand }
145
-                videoMediaState = { _isVideoMuted ? MEDIA_STATE.MUTED : MEDIA_STATE.UNMUTED } />
152
+                videoMediaState = { _videoMediaState } />
146 153
         );
147 154
     }
148 155
 }
@@ -161,19 +168,21 @@ function mapStateToProps(state, ownProps): Object {
161 168
     const localParticipantId = getLocalParticipant(state).id;
162 169
     const participant = getParticipantByIdOrUndefined(state, participantID);
163 170
     const _isAudioMuted = isParticipantAudioMuted(participant, state);
164
-    const isVideoMuted = isParticipantVideoMuted(participant, state);
171
+    const _isVideoMuted = isParticipantVideoMuted(participant, state);
165 172
     const audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
173
+    const videoMediaState = getParticipantVideoMediaState(participant, _isVideoMuted, state);
166 174
 
167 175
     return {
168 176
         _audioMediaState: audioMediaState,
169 177
         _displayName: getParticipantDisplayName(state, participant?.id),
170 178
         _isAudioMuted,
171 179
         _isFakeParticipant: Boolean(participant?.isFakeParticipant),
172
-        _isVideoMuted: isVideoMuted,
180
+        _isModerator: isParticipantModerator(participant),
173 181
         _local: Boolean(participant?.local),
174 182
         _localVideoOwner: Boolean(ownerId === localParticipantId),
175 183
         _participantID: participant?.id,
176
-        _raisedHand: Boolean(participant?.raisedHand)
184
+        _raisedHand: Boolean(participant?.raisedHand),
185
+        _videoMediaState: videoMediaState
177 186
     };
178 187
 }
179 188
 

+ 14
- 5
react/features/participants-pane/components/native/ParticipantItem.js View File

@@ -34,6 +34,11 @@ type Props = {
34 34
      */
35 35
     isKnockingParticipant: boolean,
36 36
 
37
+    /**
38
+     * Whether or not the user is a moderator.
39
+     */
40
+    isModerator?: boolean,
41
+
37 42
     /**
38 43
      * True if the participant is local.
39 44
      */
@@ -69,6 +74,7 @@ function ParticipantItem({
69 74
     children,
70 75
     displayName,
71 76
     isKnockingParticipant,
77
+    isModerator,
72 78
     local,
73 79
     onPress,
74 80
     participantID,
@@ -88,11 +94,14 @@ function ParticipantItem({
88 94
                     className = 'participant-avatar'
89 95
                     participantId = { participantID }
90 96
                     size = { 32 } />
91
-                <View style = { styles.participantNameContainer }>
92
-                    <Text style = { styles.participantName }>
93
-                        { displayName }
94
-                    </Text>
95
-                    { local ? <Text style = { styles.isLocal }>({t('chat.you')})</Text> : null }
97
+                <View style = { styles.participantDetailsContainer }>
98
+                    <View style = { styles.participantNameContainer }>
99
+                        <Text style = { styles.participantName }>
100
+                            { displayName }
101
+                        </Text>
102
+                        { local ? <Text style = { styles.isLocal }>({t('chat.you')})</Text> : null }
103
+                    </View>
104
+                    {isModerator && <Text style = { styles.moderatorLabel }>{t('videothumbnail.moderator')}</Text>}
96 105
                 </View>
97 106
                 {
98 107
                     !isKnockingParticipant

+ 8
- 19
react/features/participants-pane/components/native/ParticipantsPane.js View File

@@ -7,10 +7,8 @@ import { Button } from 'react-native-paper';
7 7
 import { useDispatch, useSelector } from 'react-redux';
8 8
 
9 9
 import { openDialog } from '../../../base/dialog';
10
-import { Icon, IconHorizontalPoints } from '../../../base/icons';
11 10
 import { JitsiModal } from '../../../base/modal';
12 11
 import {
13
-    getParticipantCount,
14 12
     isLocalParticipantModerator
15 13
 } from '../../../base/participants';
16 14
 import MuteEveryoneDialog
@@ -18,6 +16,7 @@ import MuteEveryoneDialog
18 16
 import { close } from '../../actions.native';
19 17
 
20 18
 import { ContextMenuMore } from './ContextMenuMore';
19
+import HorizontalDotsIcon from './HorizontalDotsIcon';
21 20
 import LobbyParticipantList from './LobbyParticipantList';
22 21
 import MeetingParticipantList from './MeetingParticipantList';
23 22
 import styles from './styles';
@@ -32,8 +31,6 @@ const ParticipantsPane = () => {
32 31
     const openMoreMenu = useCallback(() => dispatch(openDialog(ContextMenuMore)), [ dispatch ]);
33 32
     const closePane = useCallback(() => dispatch(close()), [ dispatch ]);
34 33
     const isLocalModerator = useSelector(isLocalParticipantModerator);
35
-    const participantsCount = useSelector(getParticipantCount);
36
-    const showContextMenu = participantsCount > 2;
37 34
     const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)),
38 35
         [ dispatch ]);
39 36
     const { t } = useTranslation();
@@ -55,21 +52,13 @@ const ParticipantsPane = () => {
55 52
                         labelStyle = { styles.muteAllLabel }
56 53
                         mode = 'contained'
57 54
                         onPress = { muteAll }
58
-                        style = { showContextMenu ? styles.muteAllMoreButton : styles.muteAllButton } />
59
-                    {
60
-                        showContextMenu
61
-                        && <Button
62
-                            /* eslint-disable-next-line react/jsx-no-bind */
63
-                            icon = { () =>
64
-                                (<Icon
65
-                                    size = { 20 }
66
-                                    src = { IconHorizontalPoints } />)
67
-                            }
68
-                            labelStyle = { styles.moreIcon }
69
-                            mode = 'contained'
70
-                            onPress = { openMoreMenu }
71
-                            style = { styles.moreButton } />
72
-                    }
55
+                        style = { styles.muteAllMoreButton } />
56
+                    <Button
57
+                        icon = { HorizontalDotsIcon }
58
+                        labelStyle = { styles.moreIcon }
59
+                        mode = 'contained'
60
+                        onPress = { openMoreMenu }
61
+                        style = { styles.moreButton } />
73 62
                 </View>
74 63
             }
75 64
         </JitsiModal>

+ 20
- 8
react/features/participants-pane/components/native/styles.js View File

@@ -78,8 +78,7 @@ const contextMenuItem = {
78 78
     display: 'flex',
79 79
     flexDirection: 'row',
80 80
     height: BaseTheme.spacing[7],
81
-    marginLeft: BaseTheme.spacing[3],
82
-    marginTop: BaseTheme.spacing[2]
81
+    marginLeft: BaseTheme.spacing[3]
83 82
 };
84 83
 
85 84
 /**
@@ -137,12 +136,18 @@ export default {
137 136
         width: '100%'
138 137
     },
139 138
 
139
+    participantDetailsContainer: {
140
+        display: 'flex',
141
+        flexDirection: 'column',
142
+        width: '100%'
143
+    },
144
+
140 145
     participantNameContainer: {
141 146
         display: 'flex',
142 147
         flexDirection: 'row',
143 148
         overflow: 'hidden',
144 149
         paddingLeft: BaseTheme.spacing[3],
145
-        width: '63%'
150
+        width: '100%'
146 151
     },
147 152
 
148 153
     participantName: {
@@ -150,6 +155,13 @@ export default {
150 155
         color: BaseTheme.palette.text01
151 156
     },
152 157
 
158
+    moderatorLabel: {
159
+        color: BaseTheme.palette.text03,
160
+        alignSelf: 'flex-start',
161
+        paddingLeft: BaseTheme.spacing[3],
162
+        paddingTop: BaseTheme.spacing[1]
163
+    },
164
+
153 165
     isLocal: {
154 166
         alignSelf: 'center',
155 167
         color: BaseTheme.palette.text01,
@@ -249,11 +261,6 @@ export default {
249 261
         marginLeft: 'auto'
250 262
     },
251 263
 
252
-    contextMenuMore: {
253
-        backgroundColor: BaseTheme.palette.bottomSheet,
254
-        borderRadius: BaseTheme.shape.borderRadius
255
-    },
256
-
257 264
     muteAllButton: {
258 265
         ...muteAllButton
259 266
     },
@@ -300,6 +307,11 @@ export default {
300 307
         marginLeft: BaseTheme.spacing[3]
301 308
     },
302 309
 
310
+    contextMenuItemTextNoIcon: {
311
+        ...contextMenuItemText,
312
+        marginLeft: BaseTheme.spacing[6]
313
+    },
314
+
303 315
     contextMenuItemName: {
304 316
         color: BaseTheme.palette.text04,
305 317
         flexShrink: 1,

+ 0
- 2
react/features/participants-pane/functions.js View File

@@ -71,7 +71,6 @@ export function isForceMuted(participant: Object, mediaType: MediaType, state: O
71 71
  * @param {Object} participant - The participant.
72 72
  * @param {boolean} muted - The mute state of the participant.
73 73
  * @param {Object} state - The redux state.
74
- * @param {boolean} ignoreDominantSpeaker - Whether to ignore the dominant speaker state.
75 74
  * @returns {MediaState}
76 75
  */
77 76
 export function getParticipantAudioMediaState(participant: Object, muted: Boolean, state: Object) {
@@ -98,7 +97,6 @@ export function getParticipantAudioMediaState(participant: Object, muted: Boolea
98 97
  * @param {Object} participant - The participant.
99 98
  * @param {boolean} muted - The mute state of the participant.
100 99
  * @param {Object} state - The redux state.
101
- * @param {boolean} ignoreDominantSpeaker - Whether to ignore the dominant speaker state.
102 100
  * @returns {MediaState}
103 101
  */
104 102
 export function getParticipantVideoMediaState(participant: Object, muted: Boolean, state: Object) {

+ 65
- 0
react/features/video-menu/components/native/AskUnmuteButton.js View File

@@ -0,0 +1,65 @@
1
+// @flow
2
+
3
+import { approveParticipant } from '../../../av-moderation/actions';
4
+import { translate } from '../../../base/i18n';
5
+import { IconMicrophone } from '../../../base/icons';
6
+import { MEDIA_TYPE } from '../../../base/media';
7
+import { getParticipantById, isLocalParticipantModerator } from '../../../base/participants';
8
+import { connect } from '../../../base/redux';
9
+import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
10
+import { isForceMuted } from '../../../participants-pane/functions';
11
+
12
+export type Props = AbstractButtonProps & {
13
+
14
+    /**
15
+     * The redux {@code dispatch} function.
16
+     */
17
+    dispatch: Function,
18
+
19
+    /**
20
+     * The ID of the participant object that this button is supposed to
21
+     * ask to unmute.
22
+     */
23
+    participantID: string,
24
+};
25
+
26
+/**
27
+ * An abstract remote video menu button which asks the remote participant to unmute.
28
+ */
29
+class AskUnmuteButton extends AbstractButton<Props, *> {
30
+    accessibilityLabel = 'participantsPane.actions.askUnmute';
31
+    icon = IconMicrophone;
32
+    label = 'participantsPane.actions.askUnmute';
33
+
34
+    /**
35
+     * Handles clicking / pressing the button, and asks the participant to unmute.
36
+     *
37
+     * @private
38
+     * @returns {void}
39
+     */
40
+    _handleClick() {
41
+        const { dispatch, participantID } = this.props;
42
+
43
+        dispatch(approveParticipant(participantID));
44
+    }
45
+}
46
+
47
+/**
48
+ * Maps part of the Redux state to the props of this component.
49
+ *
50
+ * @param {Object} state - The Redux state.
51
+ * @param {Object} ownProps - Properties of component.
52
+ * @returns {Props}
53
+ */
54
+function mapStateToProps(state, ownProps) {
55
+    const { participantID } = ownProps;
56
+    const participant = getParticipantById(state, participantID);
57
+
58
+    return {
59
+        visible: isLocalParticipantModerator(state)
60
+            && (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)
61
+            || isForceMuted(participant, MEDIA_TYPE.VIDEO, state))
62
+    };
63
+}
64
+
65
+export default translate(connect(mapStateToProps)(AskUnmuteButton));

+ 8
- 3
react/features/video-menu/components/native/GrantModeratorDialog.js View File

@@ -1,11 +1,12 @@
1 1
 // @flow
2 2
 
3 3
 import React from 'react';
4
+import { Text } from 'react-native';
4 5
 
5 6
 import { ConfirmDialog } from '../../../base/dialog';
6 7
 import { translate } from '../../../base/i18n';
7 8
 import { connect } from '../../../base/redux';
8
-import AbstractGrantModeratorDialog
9
+import AbstractGrantModeratorDialog, { abstractMapStateToProps }
9 10
     from '../AbstractGrantModeratorDialog';
10 11
 
11 12
 /**
@@ -22,11 +23,15 @@ class GrantModeratorDialog extends AbstractGrantModeratorDialog {
22 23
         return (
23 24
             <ConfirmDialog
24 25
                 contentKey = 'dialog.grantModeratorDialog'
25
-                onSubmit = { this._onSubmit } />
26
+                onSubmit = { this._onSubmit }>
27
+                <Text>
28
+                    {`${this.props.t('dialog.grantModeratorDialog', { participantName: this.props.participantName })}`}
29
+                </Text>
30
+            </ConfirmDialog>
26 31
         );
27 32
     }
28 33
 
29 34
     _onSubmit: () => boolean;
30 35
 }
31 36
 
32
-export default translate(connect()(GrantModeratorDialog));
37
+export default translate(connect(abstractMapStateToProps)(GrantModeratorDialog));

+ 36
- 2
react/features/video-menu/components/native/MuteEveryoneDialog.js View File

@@ -1,7 +1,8 @@
1 1
 // @flow
2 2
 
3 3
 import React from 'react';
4
-import { Text } from 'react-native';
4
+import { Text, View, Switch } from 'react-native';
5
+import { Divider } from 'react-native-paper';
5 6
 
6 7
 import { ColorSchemeRegistry } from '../../../base/color-scheme';
7 8
 import { ConfirmDialog } from '../../../base/dialog';
@@ -12,6 +13,8 @@ import AbstractMuteEveryoneDialog, {
12 13
     abstractMapStateToProps,
13 14
     type Props as AbstractProps } from '../AbstractMuteEveryoneDialog';
14 15
 
16
+import styles from './styles';
17
+
15 18
 type Props = AbstractProps & {
16 19
 
17 20
     /**
@@ -28,6 +31,22 @@ type Props = AbstractProps & {
28 31
  */
29 32
 class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
30 33
 
34
+    /**
35
+     * Toggles advanced moderation switch.
36
+     *
37
+     * @returns {void}
38
+     */
39
+    _onToggleModeration() {
40
+        this.setState(state => {
41
+            return {
42
+                audioModerationEnabled: !state.audioModerationEnabled,
43
+                content: this.props.t(state.audioModerationEnabled
44
+                    ? 'dialog.muteEveryoneDialog' : 'dialog.muteEveryoneDialogModerationOn'
45
+                )
46
+            };
47
+        });
48
+    }
49
+
31 50
     /**
32 51
      * Implements {@code Component#render}.
33 52
      *
@@ -39,8 +58,23 @@ class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
39 58
                 okKey = 'dialog.muteParticipantButton'
40 59
                 onSubmit = { this._onSubmit } >
41 60
                 <Text style = { this.props._dialogStyles.text }>
42
-                    { `${this.props.title} \n\n ${this.props.content}` }
61
+                    { `${this.props.title} \n\n ${this.state.content}` }
43 62
                 </Text>
63
+                {this.props.exclude.length === 0 && <>
64
+                    <Divider style = { styles.dividerWithSpacing } />
65
+                    <View style = { styles.toggleContainer }>
66
+                        <Text
67
+                            style = {{
68
+                                ...this.props._dialogStyles.text,
69
+                                ...styles.toggleLabel
70
+                            }}>
71
+                            {this.props.t('dialog.moderationAudioLabel')}
72
+                        </Text>
73
+                        <Switch
74
+                            onValueChange = { this._onToggleModeration }
75
+                            value = { !this.state.audioModerationEnabled } />
76
+                    </View>
77
+                </>}
44 78
             </ConfirmDialog>
45 79
         );
46 80
     }

+ 35
- 3
react/features/video-menu/components/native/MuteEveryonesVideoDialog.js View File

@@ -1,7 +1,8 @@
1 1
 // @flow
2 2
 
3 3
 import React from 'react';
4
-import { Text } from 'react-native';
4
+import { Switch, Text, View } from 'react-native';
5
+import { Divider } from 'react-native-paper';
5 6
 
6 7
 import { ColorSchemeRegistry } from '../../../base/color-scheme';
7 8
 import { ConfirmDialog } from '../../../base/dialog';
@@ -12,6 +13,8 @@ import AbstractMuteEveryonesVideoDialog, {
12 13
     abstractMapStateToProps,
13 14
     type Props as AbstractProps } from '../AbstractMuteEveryonesVideoDialog';
14 15
 
16
+import styles from './styles';
17
+
15 18
 type Props = AbstractProps & {
16 19
 
17 20
     /**
@@ -24,10 +27,26 @@ type Props = AbstractProps & {
24 27
  * A React Component with the contents for a dialog that asks for confirmation
25 28
  * from the user before muting all remote participants.
26 29
  *
27
- * @extends AbstractMuteEveryoneDialog
30
+ * @extends AbstractMuteEveryonesVideoDialog
28 31
  */
29 32
 class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> {
30 33
 
34
+    /**
35
+     * Toggles advanced moderation switch.
36
+     *
37
+     * @returns {void}
38
+     */
39
+    _onToggleModeration() {
40
+        this.setState(state => {
41
+            return {
42
+                moderationEnabled: !state.moderationEnabled,
43
+                content: this.props.t(state.moderationEnabled
44
+                    ? 'dialog.muteEveryonesVideoDialog' : 'dialog.muteEveryonesVideoDialogModerationOn'
45
+                )
46
+            };
47
+        });
48
+    }
49
+
31 50
     /**
32 51
      * Implements {@code Component#render}.
33 52
      *
@@ -39,8 +58,21 @@ class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> {
39 58
                 okKey = 'dialog.muteEveryonesVideoDialogOk'
40 59
                 onSubmit = { this._onSubmit } >
41 60
                 <Text style = { this.props._dialogStyles.text }>
42
-                    { `${this.props.title} \n\n ${this.props.content}` }
61
+                    { `${this.props.title} \n\n ${this.state.content}` }
43 62
                 </Text>
63
+                {this.props.exclude.length === 0 && <>
64
+                    <Divider style = { styles.dividerWithSpacing } />
65
+                    <View style = { styles.toggleContainer }>
66
+                        <Text
67
+                            style = {{ ...this.props._dialogStyles.text,
68
+                                ...styles.toggleLabel }}>
69
+                            {this.props.t('dialog.moderationVideoLabel')}
70
+                        </Text>
71
+                        <Switch
72
+                            onValueChange = { this._onToggleModeration }
73
+                            value = { !this.state.moderationEnabled } />
74
+                    </View>
75
+                </>}
44 76
             </ConfirmDialog>
45 77
         );
46 78
     }

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

@@ -18,6 +18,7 @@ import { PrivateMessageButton } from '../../../chat';
18 18
 import { hideRemoteVideoMenu } from '../../actions.native';
19 19
 import ConnectionStatusButton from '../native/ConnectionStatusButton';
20 20
 
21
+import AskUnmuteButton from './AskUnmuteButton';
21 22
 import GrantModeratorButton from './GrantModeratorButton';
22 23
 import KickButton from './KickButton';
23 24
 import MuteButton from './MuteButton';
@@ -126,6 +127,7 @@ class RemoteVideoMenu extends PureComponent<Props> {
126 127
                 onCancel = { this._onCancel }
127 128
                 renderHeader = { this._renderMenuHeader }
128 129
                 showSlidingView = { _isParticipantAvailable }>
130
+                <AskUnmuteButton { ...buttonProps } />
129 131
                 { !_disableRemoteMute && <MuteButton { ...buttonProps } /> }
130 132
                 <MuteEveryoneElseButton { ...buttonProps } />
131 133
                 { !_disableRemoteMute && <MuteVideoButton { ...buttonProps } /> }

+ 19
- 0
react/features/video-menu/components/native/styles.js View File

@@ -67,5 +67,24 @@ export default createStyleSheet({
67 67
 
68 68
     divider: {
69 69
         backgroundColor: BaseTheme.palette.dividerColor
70
+    },
71
+
72
+    dividerWithSpacing: {
73
+        backgroundColor: BaseTheme.palette.dividerColor,
74
+        marginVertical: BaseTheme.spacing[3]
75
+    },
76
+
77
+    toggleContainer: {
78
+        display: 'flex',
79
+        alignItems: 'center',
80
+        flexDirection: 'row',
81
+        justifyContent: 'center',
82
+        width: '100%',
83
+        overflow: 'hidden'
84
+    },
85
+
86
+    toggleLabel: {
87
+        marginRight: BaseTheme.spacing[3],
88
+        maxWidth: '70%'
70 89
     }
71 90
 });

Loading…
Cancel
Save