소스 검색

feat: (moderate-reaction-sounds) enable moderator to mute reaction sounds

master
Andrei Oltean 3 년 전
부모
커밋
a077043f1b
27개의 변경된 파일576개의 추가작업 그리고 193개의 파일을 삭제
  1. 5
    0
      css/modals/settings/_settings.scss
  2. 4
    1
      lang/main.json
  3. 5
    5
      react/features/av-moderation/middleware.js
  4. 11
    0
      react/features/base/conference/actionTypes.js
  5. 18
    1
      react/features/base/conference/actions.js
  6. 5
    1
      react/features/base/conference/reducer.js
  7. 7
    3
      react/features/base/devices/middleware.js
  8. 2
    2
      react/features/base/participants/middleware.js
  9. 2
    1
      react/features/base/settings/actionTypes.js
  10. 3
    1
      react/features/base/settings/actions.js
  11. 3
    3
      react/features/no-audio-signal/middleware.js
  12. 2
    2
      react/features/notifications/components/AbstractNotification.js
  13. 7
    7
      react/features/notifications/components/web/Notification.js
  14. 16
    1
      react/features/participants-pane/components/web/FooterContextMenu.js
  15. 7
    0
      react/features/reactions/constants.js
  16. 87
    7
      react/features/reactions/middleware.js
  17. 45
    0
      react/features/reactions/subscriber.js
  18. 2
    3
      react/features/recording/actions.any.js
  19. 49
    14
      react/features/settings/actions.js
  20. 18
    0
      react/features/settings/components/AbstractSettingsView.js
  21. 185
    0
      react/features/settings/components/web/ModeratorTab.js
  22. 12
    109
      react/features/settings/components/web/MoreTab.js
  23. 40
    8
      react/features/settings/components/web/SettingsDialog.js
  24. 7
    0
      react/features/settings/components/web/SoundsTab.js
  25. 1
    0
      react/features/settings/constants.js
  26. 31
    22
      react/features/settings/functions.js
  27. 2
    2
      react/features/talk-while-muted/middleware.js

+ 5
- 0
css/modals/settings/_settings.scss 파일 보기

@@ -65,6 +65,11 @@
65 65
         text-align: left;
66 66
         flex: 1;
67 67
     }
68
+
69
+    .moderator-settings-wrapper {
70
+        padding-top: 20px;
71
+    }
72
+
68 73
     .profile-edit-field {
69 74
         margin-right: 20px;
70 75
     }

+ 4
- 1
lang/main.json 파일 보기

@@ -633,6 +633,7 @@
633 633
         "moderationToggleDescription": "by {{participantDisplayName}}",
634 634
         "raiseHandAction": "Raise hand",
635 635
         "reactionSounds": "Disable sounds",
636
+        "reactionSoundsForAll": "Disable sounds for all",
636 637
         "groupTitle": "Notifications",
637 638
         "videoUnmuteBlockedTitle": "Camera unmute blocked!",
638 639
         "videoUnmuteBlockedDescription": "Camera unmute operation has been temporarily blocked because of system limits."
@@ -660,7 +661,8 @@
660 661
             "stopEveryonesVideo": "Stop everyone's video",
661 662
             "stopVideo": "Stop video",
662 663
             "unblockEveryoneMicCamera": "Unblock everyone's mic and camera",
663
-            "videoModeration": "Start their video"
664
+            "videoModeration": "Start their video",
665
+            "moreModerationControls": "More moderation controls"
664 666
         },
665 667
         "search": "Search participants"
666 668
     },
@@ -855,6 +857,7 @@
855 857
         "sounds": "Sounds",
856 858
         "speakers": "Speakers",
857 859
         "startAudioMuted": "Everyone starts muted",
860
+        "startReactionsMuted": "Mute reaction sounds for everyone",
858 861
         "startVideoMuted": "Everyone starts hidden",
859 862
         "talkWhileMuted": "Talk while muted",
860 863
         "title": "Settings"

+ 5
- 5
react/features/av-moderation/middleware.js 파일 보기

@@ -98,11 +98,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
98 98
         }
99 99
 
100 100
         dispatch(showNotification({
101
-            customActionNameKey: 'notify.raiseHandAction',
102
-            customActionHandler: () => batch(() => {
101
+            customActionNameKey: [ 'notify.raiseHandAction' ],
102
+            customActionHandler: [ () => batch(() => {
103 103
                 dispatch(raiseHand(true));
104 104
                 dispatch(hideNotification(uid));
105
-            }),
105
+            }) ],
106 106
             descriptionKey,
107 107
             sticky: true,
108 108
             titleKey,
@@ -221,8 +221,8 @@ StateListenerRegistry.register(
221 221
                     dispatch(showNotification({
222 222
                         titleKey: 'notify.hostAskedUnmute',
223 223
                         sticky: true,
224
-                        customActionNameKey: 'notify.unmute',
225
-                        customActionHandler: () => dispatch(muteLocal(false, MEDIA_TYPE.AUDIO))
224
+                        customActionNameKey: [ 'notify.unmute' ],
225
+                        customActionHandler: [ () => dispatch(muteLocal(false, MEDIA_TYPE.AUDIO)) ]
226 226
                     }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
227 227
                     dispatch(playSound(ASKED_TO_UNMUTE_SOUND_ID));
228 228
                 }

+ 11
- 0
react/features/base/conference/actionTypes.js 파일 보기

@@ -172,6 +172,17 @@ export const SEND_TONES = 'SEND_TONES';
172 172
  */
173 173
 export const SET_FOLLOW_ME = 'SET_FOLLOW_ME';
174 174
 
175
+/**
176
+ * The type of (redux) action which updates the current known status of the
177
+ * Mute Reactions Sound feature.
178
+ *
179
+ * {
180
+ *     type: SET_START_REACTIONS_MUTED,
181
+ *     enabled: boolean
182
+ * }
183
+ */
184
+export const SET_START_REACTIONS_MUTED = 'SET_START_REACTIONS_MUTED';
185
+
175 186
 /**
176 187
  * The type of (redux) action which sets the password to join or lock a specific
177 188
  * {@code JitsiConference}.

+ 18
- 1
react/features/base/conference/actions.js 파일 보기

@@ -53,7 +53,8 @@ import {
53 53
     SET_PASSWORD_FAILED,
54 54
     SET_ROOM,
55 55
     SET_PENDING_SUBJECT_CHANGE,
56
-    SET_START_MUTED_POLICY
56
+    SET_START_MUTED_POLICY,
57
+    SET_START_REACTIONS_MUTED
57 58
 } from './actionTypes';
58 59
 import {
59 60
     AVATAR_URL_COMMAND,
@@ -669,6 +670,22 @@ export function setFollowMe(enabled: boolean) {
669 670
     };
670 671
 }
671 672
 
673
+/**
674
+ * Enables or disables the Mute reaction sounds feature.
675
+ *
676
+ * @param {boolean} muted - Whether or not reaction sounds should be muted for all participants.
677
+ * @returns {{
678
+ *     type: SET_START_REACTIONS_MUTED,
679
+ *     muted: boolean
680
+ * }}
681
+ */
682
+export function setStartReactionsMuted(muted: boolean) {
683
+    return {
684
+        type: SET_START_REACTIONS_MUTED,
685
+        muted
686
+    };
687
+}
688
+
672 689
 /**
673 690
  * Sets the password to join or lock a specific JitsiConference.
674 691
  *

+ 5
- 1
react/features/base/conference/reducer.js 파일 보기

@@ -20,7 +20,8 @@ import {
20 20
     SET_PASSWORD,
21 21
     SET_PENDING_SUBJECT_CHANGE,
22 22
     SET_ROOM,
23
-    SET_START_MUTED_POLICY
23
+    SET_START_MUTED_POLICY,
24
+    SET_START_REACTIONS_MUTED
24 25
 } from './actionTypes';
25 26
 import { isRoomValid } from './functions';
26 27
 
@@ -77,6 +78,9 @@ ReducerRegistry.register(
77 78
         case SET_FOLLOW_ME:
78 79
             return set(state, 'followMeEnabled', action.enabled);
79 80
 
81
+        case SET_START_REACTIONS_MUTED:
82
+            return set(state, 'startReactionsMuted', action.muted);
83
+
80 84
         case SET_LOCATION_URL:
81 85
             return set(state, 'room', undefined);
82 86
 

+ 7
- 3
react/features/base/devices/middleware.js 파일 보기

@@ -2,7 +2,11 @@
2 2
 
3 3
 import UIEvents from '../../../../service/UI/UIEvents';
4 4
 import { processExternalDeviceRequest } from '../../device-selection';
5
-import { NOTIFICATION_TIMEOUT_TYPE, showNotification, showWarningNotification } from '../../notifications';
5
+import {
6
+    NOTIFICATION_TIMEOUT_TYPE,
7
+    showNotification,
8
+    showWarningNotification
9
+} from '../../notifications';
6 10
 import { replaceAudioTrackById, replaceVideoTrackById, setDeviceStatusWarning } from '../../prejoin/actions';
7 11
 import { isPrejoinPageVisible } from '../../prejoin/functions';
8 12
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
@@ -294,8 +298,8 @@ function _checkAndNotifyForNewDevice(store, newDevices, oldDevices) {
294 298
             dispatch(showNotification({
295 299
                 description,
296 300
                 titleKey,
297
-                customActionNameKey: 'notify.newDeviceAction',
298
-                customActionHandler: _useDevice.bind(undefined, store, devicesArray)
301
+                customActionNameKey: [ 'notify.newDeviceAction' ],
302
+                customActionHandler: [ _useDevice.bind(undefined, store, devicesArray) ]
299 303
             }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
300 304
         }
301 305
     });

+ 2
- 2
react/features/base/participants/middleware.js 파일 보기

@@ -555,8 +555,8 @@ function _raiseHandUpdated({ dispatch, getState }, conference, participantId, ne
555 555
     }
556 556
 
557 557
     const action = shouldDisplayAllowAction ? {
558
-        customActionNameKey: 'notify.allowAction',
559
-        customActionHandler: () => dispatch(approveParticipant(participantId))
558
+        customActionNameKey: [ 'notify.allowAction' ],
559
+        customActionHandler: [ () => dispatch(approveParticipant(participantId)) ]
560 560
     } : {};
561 561
 
562 562
     if (raisedHandTimestamp) {

+ 2
- 1
react/features/base/settings/actionTypes.js 파일 보기

@@ -14,7 +14,8 @@
14 14
  *         serverURL: string,
15 15
  *         startAudioOnly: boolean,
16 16
  *         startWithAudioMuted: boolean,
17
- *         startWithVideoMuted: boolean
17
+ *         startWithVideoMuted: boolean,
18
+ *         startWithReactionsMuted: boolean
18 19
  *     }
19 20
  * }
20 21
  */

+ 3
- 1
react/features/base/settings/actions.js 파일 보기

@@ -15,9 +15,11 @@ import { SETTINGS_UPDATED } from './actionTypes';
15 15
  *         localFlipX: boolean,
16 16
  *         micDeviceId: string,
17 17
  *         serverURL: string,
18
+ *         soundsReactions: boolean,
18 19
  *         startAudioOnly: boolean,
19 20
  *         startWithAudioMuted: boolean,
20
- *         startWithVideoMuted: boolean
21
+ *         startWithVideoMuted: boolean,
22
+ *         startWithReactionsMuted: boolean
21 23
  *     }
22 24
  * }}
23 25
  */

+ 3
- 3
react/features/no-audio-signal/middleware.js 파일 보기

@@ -94,8 +94,8 @@ async function _handleNoAudioSignalNotification({ dispatch, getState }, action)
94 94
             // at the point of the implementation the showNotification function only supports doing that for
95 95
             // the description.
96 96
             // TODO Add support for arguments to showNotification title and customAction strings.
97
-            customActionNameKey = `Switch to ${formatDeviceLabel(activeDevice.deviceLabel)}`;
98
-            customActionHandler = () => {
97
+            customActionNameKey = [ `Switch to ${formatDeviceLabel(activeDevice.deviceLabel)}` ];
98
+            customActionHandler = [ () => {
99 99
                 // Select device callback
100 100
                 dispatch(
101 101
                         updateSettings({
@@ -105,7 +105,7 @@ async function _handleNoAudioSignalNotification({ dispatch, getState }, action)
105 105
                 );
106 106
 
107 107
                 dispatch(setAudioInputDevice(activeDevice.deviceId));
108
-            };
108
+            } ];
109 109
         }
110 110
 
111 111
         const notification = await dispatch(showNotification({

+ 2
- 2
react/features/notifications/components/AbstractNotification.js 파일 보기

@@ -20,12 +20,12 @@ export type Props = {
20 20
     /**
21 21
      * Callback invoked when the custom button is clicked.
22 22
      */
23
-    customActionHandler: Function,
23
+    customActionHandler: Function[],
24 24
 
25 25
     /**
26 26
      * The text to display as button in the notification for the custom action.
27 27
      */
28
-    customActionNameKey: string,
28
+    customActionNameKey: string[],
29 29
 
30 30
     /**
31 31
      * The text to display in the body of the notification. If not passed

+ 7
- 7
react/features/notifications/components/web/Notification.js 파일 보기

@@ -128,17 +128,17 @@ class Notification extends AbstractNotification<Props> {
128 128
             ];
129 129
 
130 130
         default:
131
-            if (this.props.customActionNameKey && this.props.customActionHandler) {
132
-                return [
133
-                    {
134
-                        content: this.props.t(this.props.customActionNameKey),
131
+            if (this.props.customActionNameKey?.length && this.props.customActionHandler?.length) {
132
+                return this.props.customActionNameKey.map((customAction: string, customActionIndex: number) => {
133
+                    return {
134
+                        content: this.props.t(customAction),
135 135
                         onClick: () => {
136
-                            if (this.props.customActionHandler()) {
136
+                            if (this.props.customActionHandler[customActionIndex]()) {
137 137
                                 this._onDismissed();
138 138
                             }
139 139
                         }
140
-                    }
141
-                ];
140
+                    };
141
+                });
142 142
             }
143 143
 
144 144
             return [];

+ 16
- 1
react/features/participants-pane/components/web/FooterContextMenu.js 파일 보기

@@ -17,12 +17,17 @@ import {
17 17
 } from '../../../av-moderation/functions';
18 18
 import { ContextMenu, ContextMenuItemGroup } from '../../../base/components';
19 19
 import { openDialog } from '../../../base/dialog';
20
-import { IconCheck, IconVideoOff } from '../../../base/icons';
20
+import {
21
+    IconCheck,
22
+    IconHorizontalPoints,
23
+    IconVideoOff
24
+} from '../../../base/icons';
21 25
 import { MEDIA_TYPE } from '../../../base/media';
22 26
 import {
23 27
     getParticipantCount,
24 28
     isEveryoneModerator
25 29
 } from '../../../base/participants';
30
+import { openSettingsDialog, SETTINGS_TABS } from '../../../settings';
26 31
 import { MuteEveryonesVideoDialog } from '../../../video-menu/components';
27 32
 
28 33
 const useStyles = makeStyles(theme => {
@@ -95,6 +100,8 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: Props
95 100
     const muteAllVideo = useCallback(
96 101
         () => dispatch(openDialog(MuteEveryonesVideoDialog)), [ dispatch ]);
97 102
 
103
+    const openModeratorSettings = () => dispatch(openSettingsDialog(SETTINGS_TABS.MODERATOR));
104
+
98 105
     const actions = [
99 106
         {
100 107
             accessibilityLabel: t('participantsPane.actions.audioModeration'),
@@ -139,6 +146,14 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: Props
139 146
                     </div>
140 147
                 </ContextMenuItemGroup>
141 148
             )}
149
+            <ContextMenuItemGroup
150
+                actions = { [ {
151
+                    accessibilityLabel: t('participantsPane.actions.moreModerationControls'),
152
+                    id: 'participants-pane-open-moderation-control-settings',
153
+                    icon: IconHorizontalPoints,
154
+                    onClick: openModeratorSettings,
155
+                    text: t('participantsPane.actions.moreModerationControls')
156
+                } ] } />
142 157
         </ContextMenu>
143 158
     );
144 159
 };

+ 7
- 0
react/features/reactions/constants.js 파일 보기

@@ -14,6 +14,13 @@ import {
14 14
  */
15 15
 export const ENDPOINT_REACTION_NAME = 'endpoint-reaction';
16 16
 
17
+/**
18
+ * The (name of the) command which transports the state (represented by
19
+ * {State} for the local state at the time of this writing) of a {MuteReactions}
20
+ * (instance) between moderator and participants.
21
+ */
22
+export const MUTE_REACTIONS_COMMAND = 'mute-reactions';
23
+
17 24
 /**
18 25
  * The prefix for all reaction sound IDs. Also the ID used in config to disable reaction sounds.
19 26
  */

+ 87
- 7
react/features/reactions/middleware.js 파일 보기

@@ -4,9 +4,15 @@ import { batch } from 'react-redux';
4 4
 
5 5
 import { createReactionSoundsDisabledEvent, sendAnalytics } from '../analytics';
6 6
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
7
-import { getParticipantCount } from '../base/participants';
7
+import { CONFERENCE_WILL_JOIN, setStartReactionsMuted } from '../base/conference';
8
+import {
9
+    getParticipantById,
10
+    getParticipantCount,
11
+    isLocalParticipantModerator
12
+} from '../base/participants';
8 13
 import { MiddlewareRegistry } from '../base/redux';
9
-import { SETTINGS_UPDATED, updateSettings } from '../base/settings';
14
+import { SETTINGS_UPDATED } from '../base/settings/actionTypes';
15
+import { updateSettings } from '../base/settings/actions';
10 16
 import { playSound, registerSound, unregisterSound } from '../base/sounds';
11 17
 import { getDisabledSounds } from '../base/sounds/functions.any';
12 18
 import { NOTIFICATION_TIMEOUT_TYPE, showNotification } from '../notifications';
@@ -31,7 +37,8 @@ import {
31 37
     RAISE_HAND_SOUND_ID,
32 38
     REACTIONS,
33 39
     REACTION_SOUND,
34
-    SOUNDS_THRESHOLDS
40
+    SOUNDS_THRESHOLDS,
41
+    MUTE_REACTIONS_COMMAND
35 42
 } from './constants';
36 43
 import {
37 44
     getReactionMessageFromBuffer,
@@ -39,8 +46,11 @@ import {
39 46
     getReactionsWithId,
40 47
     sendReactionsWebhook
41 48
 } from './functions.any';
49
+import logger from './logger';
42 50
 import { RAISE_HAND_SOUND_FILE } from './sounds';
43 51
 
52
+import './subscriber';
53
+
44 54
 
45 55
 declare var APP: Object;
46 56
 
@@ -95,7 +105,15 @@ MiddlewareRegistry.register(store => next => action => {
95 105
 
96 106
         break;
97 107
     }
108
+    case CONFERENCE_WILL_JOIN: {
109
+        const { conference } = action;
98 110
 
111
+        conference.addCommandListener(
112
+            MUTE_REACTIONS_COMMAND, ({ attributes }, id) => {
113
+                _onMuteReactionsCommand(attributes, id, store);
114
+            });
115
+        break;
116
+    }
99 117
     case FLUSH_REACTION_BUFFER: {
100 118
         const state = getState();
101 119
         const { buffer } = state['features/reactions'];
@@ -163,12 +181,26 @@ MiddlewareRegistry.register(store => next => action => {
163 181
     }
164 182
 
165 183
     case SHOW_SOUNDS_NOTIFICATION: {
184
+        const state = getState();
185
+        const isModerator = isLocalParticipantModerator(state);
186
+
187
+        const customActions = [ 'notify.reactionSounds' ];
188
+        const customFunctions = [ () => dispatch(updateSettings({
189
+            soundsReactions: false
190
+        })) ];
191
+
192
+        if (isModerator) {
193
+            customActions.push('notify.reactionSoundsForAll');
194
+            customFunctions.push(() => batch(() => {
195
+                dispatch(setStartReactionsMuted(true));
196
+                dispatch(updateSettings({ soundsReactions: false }));
197
+            }));
198
+        }
199
+
166 200
         dispatch(showNotification({
167 201
             titleKey: 'toolbar.disableReactionSounds',
168
-            customActionNameKey: 'notify.reactionSounds',
169
-            customActionHandler: () => dispatch(updateSettings({
170
-                soundsReactions: false
171
-            }))
202
+            customActionNameKey: customActions,
203
+            customActionHandler: customFunctions
172 204
         }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
173 205
         break;
174 206
     }
@@ -176,3 +208,51 @@ MiddlewareRegistry.register(store => next => action => {
176 208
 
177 209
     return next(action);
178 210
 });
211
+
212
+/**
213
+ * Notifies this instance about a "Mute Reaction Sounds" command received by the Jitsi
214
+ * conference.
215
+ *
216
+ * @param {Object} attributes - The attributes carried by the command.
217
+ * @param {string} id - The identifier of the participant who issuing the
218
+ * command. A notable idiosyncrasy to be mindful of here is that the command
219
+ * may be issued by the local participant.
220
+ * @param {Object} store - The redux store. Used to calculate and dispatch
221
+ * updates.
222
+ * @private
223
+ * @returns {void}
224
+ */
225
+function _onMuteReactionsCommand(attributes = {}, id, store) {
226
+    const state = store.getState();
227
+
228
+    // We require to know who issued the command because (1) only a
229
+    // moderator is allowed to send commands and (2) a command MUST be
230
+    // issued by a defined commander.
231
+    if (typeof id === 'undefined') {
232
+        return;
233
+    }
234
+
235
+    const participantSendingCommand = getParticipantById(state, id);
236
+
237
+    // The Command(s) API will send us our own commands and we don't want
238
+    // to act upon them.
239
+    if (participantSendingCommand.local) {
240
+        return;
241
+    }
242
+
243
+    if (participantSendingCommand.role !== 'moderator') {
244
+        logger.warn('Received mute-reactions command not from moderator');
245
+
246
+        return;
247
+    }
248
+
249
+    const oldState = Boolean(state['features/base/conference'].startReactionsMuted);
250
+    const newState = attributes.startReactionsMuted === 'true';
251
+
252
+    if (oldState !== newState) {
253
+        batch(() => {
254
+            store.dispatch(setStartReactionsMuted(newState));
255
+            store.dispatch(updateSettings({ soundsReactions: !newState }));
256
+        });
257
+    }
258
+}

+ 45
- 0
react/features/reactions/subscriber.js 파일 보기

@@ -0,0 +1,45 @@
1
+// @flow
2
+
3
+import { getCurrentConference } from '../base/conference';
4
+import { isLocalParticipantModerator } from '../base/participants';
5
+import { StateListenerRegistry } from '../base/redux';
6
+
7
+import { MUTE_REACTIONS_COMMAND } from './constants';
8
+
9
+/**
10
+ * Subscribes to changes to the Mute Reaction Sounds setting for the local participant to
11
+ * notify remote participants of current user interface status.
12
+ * Changing newSelectedValue param to off, when feature is turned of so we can
13
+ * notify all listeners.
14
+ */
15
+StateListenerRegistry.register(
16
+    /* selector */ state => state['features/base/conference'].startReactionsMuted,
17
+    /* listener */ (newSelectedValue, store) => _sendMuteReactionsCommand(newSelectedValue || false, store));
18
+
19
+
20
+/**
21
+ * Sends the mute-reactions command, when a local property change occurs.
22
+ *
23
+ * @param {*} newSelectedValue - The changed selected value from the selector.
24
+ * @param {Object} store - The redux store.
25
+ * @private
26
+ * @returns {void}
27
+ */
28
+function _sendMuteReactionsCommand(newSelectedValue, store) {
29
+    const state = store.getState();
30
+    const conference = getCurrentConference(state);
31
+
32
+    if (!conference) {
33
+        return;
34
+    }
35
+
36
+    // Only a moderator is allowed to send commands.
37
+    if (!isLocalParticipantModerator(state)) {
38
+        return;
39
+    }
40
+
41
+    conference.sendCommand(
42
+        MUTE_REACTIONS_COMMAND,
43
+        { attributes: { startReactionsMuted: Boolean(newSelectedValue) } }
44
+    );
45
+}

+ 2
- 3
react/features/recording/actions.any.js 파일 보기

@@ -170,7 +170,6 @@ export function showStartedRecordingNotification(
170 170
         const initiatorId = getResourceId(initiator);
171 171
         const participantName = getParticipantDisplayName(state, initiatorId);
172 172
         let dialogProps = {
173
-            customActionNameKey: undefined,
174 173
             descriptionKey: participantName ? 'liveStreaming.onBy' : 'liveStreaming.on',
175 174
             descriptionArguments: { name: participantName },
176 175
             isDismissAllowed: true,
@@ -206,8 +205,8 @@ export function showStartedRecordingNotification(
206 205
                     }
207 206
 
208 207
                     // add the option to copy recording link
209
-                    dialogProps.customActionNameKey = 'recording.copyLink';
210
-                    dialogProps.customActionHandler = () => copyText(link);
208
+                    dialogProps.customActionNameKey = [ 'recording.copyLink' ];
209
+                    dialogProps.customActionHandler = [ () => copyText(link) ];
211 210
                     dialogProps.titleKey = 'recording.on';
212 211
                     dialogProps.descriptionKey = 'recording.linkGenerated';
213 212
                     dialogProps.isDismissAllowed = false;

+ 49
- 14
react/features/settings/actions.js 파일 보기

@@ -1,6 +1,11 @@
1 1
 // @flow
2
+import { batch } from 'react-redux';
2 3
 
3
-import { setFollowMe, setStartMutedPolicy } from '../base/conference';
4
+import {
5
+    setFollowMe,
6
+    setStartMutedPolicy,
7
+    setStartReactionsMuted
8
+} from '../base/conference';
4 9
 import { openDialog } from '../base/dialog';
5 10
 import { i18next } from '../base/i18n';
6 11
 import { updateSettings } from '../base/settings';
@@ -12,7 +17,12 @@ import {
12 17
     SET_VIDEO_SETTINGS_VISIBILITY
13 18
 } from './actionTypes';
14 19
 import { LogoutDialog, SettingsDialog } from './components';
15
-import { getMoreTabProps, getProfileTabProps, getSoundsTabProps } from './functions';
20
+import {
21
+    getModeratorTabProps,
22
+    getMoreTabProps,
23
+    getProfileTabProps,
24
+    getSoundsTabProps
25
+} from './functions';
16 26
 
17 27
 declare var APP: Object;
18 28
 
@@ -74,10 +84,6 @@ export function submitMoreTab(newState: Object): Function {
74 84
     return (dispatch, getState) => {
75 85
         const currentState = getMoreTabProps(getState());
76 86
 
77
-        if (newState.followMeEnabled !== currentState.followMeEnabled) {
78
-            dispatch(setFollowMe(newState.followMeEnabled));
79
-        }
80
-
81 87
         const showPrejoinPage = newState.showPrejoinPage;
82 88
 
83 89
         if (showPrejoinPage !== currentState.showPrejoinPage) {
@@ -91,12 +97,6 @@ export function submitMoreTab(newState: Object): Function {
91 97
             }));
92 98
         }
93 99
 
94
-        if (newState.startAudioMuted !== currentState.startAudioMuted
95
-            || newState.startVideoMuted !== currentState.startVideoMuted) {
96
-            dispatch(setStartMutedPolicy(
97
-                newState.startAudioMuted, newState.startVideoMuted));
98
-        }
99
-
100 100
         if (newState.currentLanguage !== currentState.currentLanguage) {
101 101
             i18next.changeLanguage(newState.currentLanguage);
102 102
         }
@@ -109,6 +109,35 @@ export function submitMoreTab(newState: Object): Function {
109 109
     };
110 110
 }
111 111
 
112
+/**
113
+ * Submits the settings from the "Moderator" tab of the settings dialog.
114
+ *
115
+ * @param {Object} newState - The new settings.
116
+ * @returns {Function}
117
+ */
118
+export function submitModeratorTab(newState: Object): Function {
119
+    return (dispatch, getState) => {
120
+        const currentState = getModeratorTabProps(getState());
121
+
122
+        if (newState.followMeEnabled !== currentState.followMeEnabled) {
123
+            dispatch(setFollowMe(newState.followMeEnabled));
124
+        }
125
+
126
+        if (newState.startReactionsMuted !== currentState.startReactionsMuted) {
127
+            batch(() => {
128
+                dispatch(setStartReactionsMuted(newState.startReactionsMuted));
129
+                dispatch(updateSettings({ soundsReactions: !newState.startReactionsMuted }));
130
+            });
131
+        }
132
+
133
+        if (newState.startAudioMuted !== currentState.startAudioMuted
134
+            || newState.startVideoMuted !== currentState.startVideoMuted) {
135
+            dispatch(setStartMutedPolicy(
136
+                newState.startAudioMuted, newState.startVideoMuted));
137
+        }
138
+    };
139
+}
140
+
112 141
 /**
113 142
  * Submits the settings from the "Profile" tab of the settings dialog.
114 143
  *
@@ -138,6 +167,7 @@ export function submitProfileTab(newState: Object): Function {
138 167
 export function submitSoundsTab(newState: Object): Function {
139 168
     return (dispatch, getState) => {
140 169
         const currentState = getSoundsTabProps(getState());
170
+        const shouldNotUpdateReactionSounds = getModeratorTabProps(getState()).startReactionsMuted;
141 171
         const shouldUpdate = (newState.soundsIncomingMessage !== currentState.soundsIncomingMessage)
142 172
             || (newState.soundsParticipantJoined !== currentState.soundsParticipantJoined)
143 173
             || (newState.soundsParticipantLeft !== currentState.soundsParticipantLeft)
@@ -145,13 +175,18 @@ export function submitSoundsTab(newState: Object): Function {
145 175
             || (newState.soundsReactions !== currentState.soundsReactions);
146 176
 
147 177
         if (shouldUpdate) {
148
-            dispatch(updateSettings({
178
+            const settingsToUpdate = {
149 179
                 soundsIncomingMessage: newState.soundsIncomingMessage,
150 180
                 soundsParticipantJoined: newState.soundsParticipantJoined,
151 181
                 soundsParticipantLeft: newState.soundsParticipantLeft,
152 182
                 soundsTalkWhileMuted: newState.soundsTalkWhileMuted,
153 183
                 soundsReactions: newState.soundsReactions
154
-            }));
184
+            };
185
+
186
+            if (shouldNotUpdateReactionSounds) {
187
+                delete settingsToUpdate.soundsReactions;
188
+            }
189
+            dispatch(updateSettings(settingsToUpdate));
155 190
         }
156 191
     };
157 192
 }

+ 18
- 0
react/features/settings/components/AbstractSettingsView.js 파일 보기

@@ -67,6 +67,8 @@ export class AbstractSettingsView<P: Props, S: *> extends Component<P, S> {
67 67
             = this._onStartAudioMutedChange.bind(this);
68 68
         this._onStartVideoMutedChange
69 69
             = this._onStartVideoMutedChange.bind(this);
70
+        this._onStartReactionsMutedChange
71
+            = this._onStartReactionsMutedChange.bind(this);
70 72
     }
71 73
 
72 74
     _onChangeDisplayName: (string) => void;
@@ -146,6 +148,22 @@ export class AbstractSettingsView<P: Props, S: *> extends Component<P, S> {
146 148
         });
147 149
     }
148 150
 
151
+    _onStartReactionsMutedChange: (boolean) => void;
152
+
153
+    /**
154
+     * Handles the start reactions muted change event.
155
+     *
156
+     * @param {boolean} newValue - The new value for the start reactions muted
157
+     * option.
158
+     * @protected
159
+     * @returns {void}
160
+     */
161
+    _onStartReactionsMutedChange(newValue) {
162
+        this._updateSettings({
163
+            startWithReactionsMuted: newValue
164
+        });
165
+    }
166
+
149 167
     _updateSettings: (Object) => void;
150 168
 
151 169
     /**

+ 185
- 0
react/features/settings/components/web/ModeratorTab.js 파일 보기

@@ -0,0 +1,185 @@
1
+// @flow
2
+import { Checkbox } from '@atlaskit/checkbox';
3
+import React from 'react';
4
+
5
+import { AbstractDialogTab } from '../../../base/dialog';
6
+import type { Props as AbstractDialogTabProps } from '../../../base/dialog';
7
+import { translate } from '../../../base/i18n';
8
+
9
+/**
10
+ * The type of the React {@code Component} props of {@link MoreTab}.
11
+ */
12
+export type Props = {
13
+    ...$Exact<AbstractDialogTabProps>,
14
+
15
+    /**
16
+     * Whether or not follow me is currently active (enabled by some other participant).
17
+     */
18
+    followMeActive: boolean,
19
+
20
+    /**
21
+     * Whether or not the user has selected the Follow Me feature to be enabled.
22
+     */
23
+    followMeEnabled: boolean,
24
+
25
+    /**
26
+     * Whether or not the user has selected the Start Audio Muted feature to be
27
+     * enabled.
28
+     */
29
+    startAudioMuted: boolean,
30
+
31
+    /**
32
+     * Whether or not the user has selected the Start Video Muted feature to be
33
+     * enabled.
34
+     */
35
+    startVideoMuted: boolean,
36
+
37
+    /**
38
+     * Whether or not the user has selected the Start Reactions Muted feature to be
39
+     * enabled.
40
+     */
41
+    startReactionsMuted: boolean,
42
+
43
+    /**
44
+     * Invoked to obtain translated strings.
45
+     */
46
+    t: Function
47
+};
48
+
49
+/**
50
+ * React {@code Component} for modifying language and moderator settings.
51
+ *
52
+ * @augments Component
53
+ */
54
+class ModeratorTab extends AbstractDialogTab<Props> {
55
+    /**
56
+     * Initializes a new {@code MoreTab} instance.
57
+     *
58
+     * @param {Object} props - The read-only properties with which the new
59
+     * instance is to be initialized.
60
+     */
61
+    constructor(props: Props) {
62
+        super(props);
63
+
64
+        // Bind event handler so it is only bound once for every instance.
65
+        this._onStartAudioMutedChanged = this._onStartAudioMutedChanged.bind(this);
66
+        this._onStartVideoMutedChanged = this._onStartVideoMutedChanged.bind(this);
67
+        this._onStartReactionsMutedChanged = this._onStartReactionsMutedChanged.bind(this);
68
+        this._onFollowMeEnabledChanged = this._onFollowMeEnabledChanged.bind(this);
69
+    }
70
+
71
+    /**
72
+     * Implements React's {@link Component#render()}.
73
+     *
74
+     * @inheritdoc
75
+     * @returns {ReactElement}
76
+     */
77
+    render() {
78
+        return <div className = 'moderator-tab box'>{ this._renderModeratorSettings() }</div>;
79
+    }
80
+
81
+    _onStartAudioMutedChanged: (Object) => void;
82
+
83
+    /**
84
+     * Callback invoked to select if conferences should start
85
+     * with audio muted.
86
+     *
87
+     * @param {Object} e - The key event to handle.
88
+     *
89
+     * @returns {void}
90
+     */
91
+    _onStartAudioMutedChanged({ target: { checked } }) {
92
+        super._onChange({ startAudioMuted: checked });
93
+    }
94
+
95
+    _onStartVideoMutedChanged: (Object) => void;
96
+
97
+    /**
98
+     * Callback invoked to select if conferences should start
99
+     * with video disabled.
100
+     *
101
+     * @param {Object} e - The key event to handle.
102
+     *
103
+     * @returns {void}
104
+     */
105
+    _onStartVideoMutedChanged({ target: { checked } }) {
106
+        super._onChange({ startVideoMuted: checked });
107
+    }
108
+
109
+    _onStartReactionsMutedChanged: (Object) => void;
110
+
111
+    /**
112
+     * Callback invoked to select if conferences should start
113
+     * with reactions muted.
114
+     *
115
+     * @param {Object} e - The key event to handle.
116
+     *
117
+     * @returns {void}
118
+     */
119
+    _onStartReactionsMutedChanged({ target: { checked } }) {
120
+        super._onChange({ startReactionsMuted: checked });
121
+    }
122
+
123
+    _onFollowMeEnabledChanged: (Object) => void;
124
+
125
+    /**
126
+     * Callback invoked to select if follow-me mode
127
+     * should be activated.
128
+     *
129
+     * @param {Object} e - The key event to handle.
130
+     *
131
+     * @returns {void}
132
+     */
133
+    _onFollowMeEnabledChanged({ target: { checked } }) {
134
+        super._onChange({ followMeEnabled: checked });
135
+    }
136
+
137
+    /**
138
+     * Returns the React Element for modifying conference-wide settings.
139
+     *
140
+     * @private
141
+     * @returns {ReactElement}
142
+     */
143
+    _renderModeratorSettings() {
144
+        const {
145
+            followMeActive,
146
+            followMeEnabled,
147
+            startAudioMuted,
148
+            startVideoMuted,
149
+            startReactionsMuted,
150
+            t
151
+        } = this.props;
152
+
153
+        return (
154
+            <div
155
+                className = 'settings-sub-pane-element'
156
+                key = 'moderator'>
157
+                <div className = 'moderator-settings-wrapper'>
158
+                    <Checkbox
159
+                        isChecked = { startAudioMuted }
160
+                        label = { t('settings.startAudioMuted') }
161
+                        name = 'start-audio-muted'
162
+                        onChange = { this._onStartAudioMutedChanged } />
163
+                    <Checkbox
164
+                        isChecked = { startVideoMuted }
165
+                        label = { t('settings.startVideoMuted') }
166
+                        name = 'start-video-muted'
167
+                        onChange = { this._onStartVideoMutedChanged } />
168
+                    <Checkbox
169
+                        isChecked = { followMeEnabled && !followMeActive }
170
+                        isDisabled = { followMeActive }
171
+                        label = { t('settings.followMe') }
172
+                        name = 'follow-me'
173
+                        onChange = { this._onFollowMeEnabledChanged } />
174
+                    <Checkbox
175
+                        isChecked = { startReactionsMuted }
176
+                        label = { t('settings.startReactionsMuted') }
177
+                        name = 'start-reactions-muted'
178
+                        onChange = { this._onStartReactionsMutedChanged } />
179
+                </div>
180
+            </div>
181
+        );
182
+    }
183
+}
184
+
185
+export default translate(ModeratorTab);

+ 12
- 109
react/features/settings/components/web/MoreTab.js 파일 보기

@@ -40,11 +40,6 @@ export type Props = {
40 40
      */
41 41
     followMeActive: boolean,
42 42
 
43
-    /**
44
-     * Whether or not the user has selected the Follow Me feature to be enabled.
45
-     */
46
-    followMeEnabled: boolean,
47
-
48 43
     /**
49 44
      * All available languages to display in the language select dropdown.
50 45
      */
@@ -70,18 +65,6 @@ export type Props = {
70 65
      */
71 66
     showPrejoinPage: boolean,
72 67
 
73
-    /**
74
-     * Whether or not the user has selected the Start Audio Muted feature to be
75
-     * enabled.
76
-     */
77
-    startAudioMuted: boolean,
78
-
79
-    /**
80
-     * Whether or not the user has selected the Start Video Muted feature to be
81
-     * enabled.
82
-     */
83
-    startVideoMuted: boolean,
84
-
85 68
     /**
86 69
      * Invoked to obtain translated strings.
87 70
      */
@@ -129,9 +112,6 @@ class MoreTab extends AbstractDialogTab<Props, State> {
129 112
         this._onFramerateItemSelect = this._onFramerateItemSelect.bind(this);
130 113
         this._onLanguageDropdownOpenChange = this._onLanguageDropdownOpenChange.bind(this);
131 114
         this._onLanguageItemSelect = this._onLanguageItemSelect.bind(this);
132
-        this._onStartAudioMutedChanged = this._onStartAudioMutedChanged.bind(this);
133
-        this._onStartVideoMutedChanged = this._onStartVideoMutedChanged.bind(this);
134
-        this._onFollowMeEnabledChanged = this._onFollowMeEnabledChanged.bind(this);
135 115
         this._onShowPrejoinPageChanged = this._onShowPrejoinPageChanged.bind(this);
136 116
         this._onKeyboardShortcutEnableChanged = this._onKeyboardShortcutEnableChanged.bind(this);
137 117
     }
@@ -148,7 +128,13 @@ class MoreTab extends AbstractDialogTab<Props, State> {
148 128
         content.push(this._renderSettingsLeft());
149 129
         content.push(this._renderSettingsRight());
150 130
 
151
-        return <div className = 'more-tab box'>{ content }</div>;
131
+        return (
132
+            <div
133
+                className = 'more-tab box'
134
+                key = 'more'>
135
+                { content }
136
+            </div>
137
+        );
152 138
     }
153 139
 
154 140
     _onFramerateDropdownOpenChange: (Object) => void;
@@ -207,48 +193,6 @@ class MoreTab extends AbstractDialogTab<Props, State> {
207 193
         super._onChange({ currentLanguage: language });
208 194
     }
209 195
 
210
-    _onStartAudioMutedChanged: (Object) => void;
211
-
212
-    /**
213
-     * Callback invoked to select if conferences should start
214
-     * with audio muted.
215
-     *
216
-     * @param {Object} e - The key event to handle.
217
-     *
218
-     * @returns {void}
219
-     */
220
-    _onStartAudioMutedChanged({ target: { checked } }) {
221
-        super._onChange({ startAudioMuted: checked });
222
-    }
223
-
224
-    _onStartVideoMutedChanged: (Object) => void;
225
-
226
-    /**
227
-     * Callback invoked to select if conferences should start
228
-     * with video disabled.
229
-     *
230
-     * @param {Object} e - The key event to handle.
231
-     *
232
-     * @returns {void}
233
-     */
234
-    _onStartVideoMutedChanged({ target: { checked } }) {
235
-        super._onChange({ startVideoMuted: checked });
236
-    }
237
-
238
-    _onFollowMeEnabledChanged: (Object) => void;
239
-
240
-    /**
241
-     * Callback invoked to select if follow-me mode
242
-     * should be activated.
243
-     *
244
-     * @param {Object} e - The key event to handle.
245
-     *
246
-     * @returns {void}
247
-     */
248
-    _onFollowMeEnabledChanged({ target: { checked } }) {
249
-        super._onChange({ followMeEnabled: checked });
250
-    }
251
-
252 196
     _onShowPrejoinPageChanged: (Object) => void;
253 197
 
254 198
     /**
@@ -410,48 +354,6 @@ class MoreTab extends AbstractDialogTab<Props, State> {
410 354
         );
411 355
     }
412 356
 
413
-    /**
414
-     * Returns the React Element for modifying conference-wide settings.
415
-     *
416
-     * @private
417
-     * @returns {ReactElement}
418
-     */
419
-    _renderModeratorSettings() {
420
-        const {
421
-            followMeActive,
422
-            followMeEnabled,
423
-            startAudioMuted,
424
-            startVideoMuted,
425
-            t
426
-        } = this.props;
427
-
428
-        return (
429
-            <div
430
-                className = 'settings-sub-pane-element'
431
-                key = 'moderator'>
432
-                <h2 className = 'mock-atlaskit-label'>
433
-                    { t('settings.moderator') }
434
-                </h2>
435
-                <Checkbox
436
-                    isChecked = { startAudioMuted }
437
-                    label = { t('settings.startAudioMuted') }
438
-                    name = 'start-audio-muted'
439
-                    onChange = { this._onStartAudioMutedChanged } />
440
-                <Checkbox
441
-                    isChecked = { startVideoMuted }
442
-                    label = { t('settings.startVideoMuted') }
443
-                    name = 'start-video-muted'
444
-                    onChange = { this._onStartVideoMutedChanged } />
445
-                <Checkbox
446
-                    isChecked = { followMeEnabled && !followMeActive }
447
-                    isDisabled = { followMeActive }
448
-                    label = { t('settings.followMe') }
449
-                    name = 'follow-me'
450
-                    onChange = { this._onFollowMeEnabledChanged } />
451
-            </div>
452
-        );
453
-    }
454
-
455 357
     /**
456 358
      * Returns the React Element for modifying prejoin screen settings.
457 359
      *
@@ -488,7 +390,8 @@ class MoreTab extends AbstractDialogTab<Props, State> {
488 390
 
489 391
         return (
490 392
             <div
491
-                className = 'settings-sub-pane right'>
393
+                className = 'settings-sub-pane right'
394
+                key = 'settings-sub-pane-right'>
492 395
                 { showLanguageSettings && this._renderLanguageSelect() }
493 396
                 { this._renderFramerateSelect() }
494 397
             </div>
@@ -501,14 +404,14 @@ class MoreTab extends AbstractDialogTab<Props, State> {
501 404
      * @returns {ReactElement}
502 405
      */
503 406
     _renderSettingsLeft() {
504
-        const { showPrejoinSettings, showModeratorSettings } = this.props;
407
+        const { showPrejoinSettings } = this.props;
505 408
 
506 409
         return (
507 410
             <div
508
-                className = 'settings-sub-pane left'>
411
+                className = 'settings-sub-pane left'
412
+                key = 'settings-sub-pane-left'>
509 413
                 { showPrejoinSettings && this._renderPrejoinScreenSettings() }
510 414
                 { this._renderKeyboardShortcutCheckbox() }
511
-                { showModeratorSettings && this._renderModeratorSettings() }
512 415
             </div>
513 416
         );
514 417
     }

+ 40
- 8
react/features/settings/components/web/SettingsDialog.js 파일 보기

@@ -11,11 +11,22 @@ import {
11 11
     getDeviceSelectionDialogProps,
12 12
     submitDeviceSelectionTab
13 13
 } from '../../../device-selection';
14
-import { submitMoreTab, submitProfileTab, submitSoundsTab } from '../../actions';
14
+import {
15
+    submitModeratorTab,
16
+    submitMoreTab,
17
+    submitProfileTab,
18
+    submitSoundsTab
19
+} from '../../actions';
15 20
 import { SETTINGS_TABS } from '../../constants';
16
-import { getMoreTabProps, getProfileTabProps, getSoundsTabProps } from '../../functions';
21
+import {
22
+    getModeratorTabProps,
23
+    getMoreTabProps,
24
+    getProfileTabProps,
25
+    getSoundsTabProps
26
+} from '../../functions';
17 27
 
18 28
 import CalendarTab from './CalendarTab';
29
+import ModeratorTab from './ModeratorTab';
19 30
 import MoreTab from './MoreTab';
20 31
 import ProfileTab from './ProfileTab';
21 32
 import SoundsTab from './SoundsTab';
@@ -131,7 +142,9 @@ function _mapStateToProps(state) {
131 142
     // The settings sections to display.
132 143
     const showDeviceSettings = configuredTabs.includes('devices');
133 144
     const moreTabProps = getMoreTabProps(state);
134
-    const { showModeratorSettings, showLanguageSettings, showPrejoinSettings } = moreTabProps;
145
+    const moderatorTabProps = getModeratorTabProps(state);
146
+    const { showModeratorSettings } = moderatorTabProps;
147
+    const { showLanguageSettings, showPrejoinSettings } = moreTabProps;
135 148
     const showProfileSettings
136 149
         = configuredTabs.includes('profile') && !state['features/base/config'].disableProfile;
137 150
     const showCalendarSettings
@@ -176,6 +189,28 @@ function _mapStateToProps(state) {
176 189
         });
177 190
     }
178 191
 
192
+    if (showModeratorSettings) {
193
+        tabs.push({
194
+            name: SETTINGS_TABS.MODERATOR,
195
+            component: ModeratorTab,
196
+            label: 'settings.moderator',
197
+            props: moderatorTabProps,
198
+            propsUpdateFunction: (tabState, newProps) => {
199
+                // Updates tab props, keeping users selection
200
+
201
+                return {
202
+                    ...newProps,
203
+                    followMeEnabled: tabState.followMeEnabled,
204
+                    startAudioMuted: tabState.startAudioMuted,
205
+                    startVideoMuted: tabState.startVideoMuted,
206
+                    startReactionsMuted: tabState.startReactionsMuted
207
+                };
208
+            },
209
+            styles: 'settings-pane moderator-pane',
210
+            submit: submitModeratorTab
211
+        });
212
+    }
213
+
179 214
     if (showCalendarSettings) {
180 215
         tabs.push({
181 216
             name: SETTINGS_TABS.CALENDAR,
@@ -196,7 +231,7 @@ function _mapStateToProps(state) {
196 231
         });
197 232
     }
198 233
 
199
-    if (showModeratorSettings || showLanguageSettings || showPrejoinSettings) {
234
+    if (showLanguageSettings || showPrejoinSettings) {
200 235
         tabs.push({
201 236
             name: SETTINGS_TABS.MORE,
202 237
             component: MoreTab,
@@ -209,10 +244,7 @@ function _mapStateToProps(state) {
209 244
                     ...newProps,
210 245
                     currentFramerate: tabState.currentFramerate,
211 246
                     currentLanguage: tabState.currentLanguage,
212
-                    followMeEnabled: tabState.followMeEnabled,
213
-                    showPrejoinPage: tabState.showPrejoinPage,
214
-                    startAudioMuted: tabState.startAudioMuted,
215
-                    startVideoMuted: tabState.startVideoMuted
247
+                    showPrejoinPage: tabState.showPrejoinPage
216 248
                 };
217 249
             },
218 250
             styles: 'settings-pane more-pane',

+ 7
- 0
react/features/settings/components/web/SoundsTab.js 파일 보기

@@ -45,6 +45,11 @@ export type Props = {
45 45
     */
46 46
     soundsReactions: Boolean,
47 47
 
48
+    /**
49
+     * Whether or not moderator muted the sounds.
50
+     */
51
+    moderatorMutedSoundsReactions: Boolean,
52
+
48 53
     /**
49 54
      * Invoked to obtain translated strings.
50 55
      */
@@ -97,6 +102,7 @@ class SoundsTab extends AbstractDialogTab<Props> {
97 102
             soundsTalkWhileMuted,
98 103
             soundsReactions,
99 104
             enableReactions,
105
+            moderatorMutedSoundsReactions,
100 106
             t
101 107
         } = this.props;
102 108
 
@@ -109,6 +115,7 @@ class SoundsTab extends AbstractDialogTab<Props> {
109 115
                 </h2>
110 116
                 {enableReactions && <Checkbox
111 117
                     isChecked = { soundsReactions }
118
+                    isDisabled = { moderatorMutedSoundsReactions }
112 119
                     label = { t('settings.reactions') }
113 120
                     name = 'soundsReactions'
114 121
                     onChange = { this._onChange } />

+ 1
- 0
react/features/settings/constants.js 파일 보기

@@ -2,6 +2,7 @@ export const SETTINGS_TABS = {
2 2
     CALENDAR: 'calendar_tab',
3 3
     DEVICES: 'devices_tab',
4 4
     MORE: 'more_tab',
5
+    MODERATOR: 'moderator-tab',
5 6
     PROFILE: 'profile_tab',
6 7
     SOUNDS: 'sounds_tab'
7 8
 };

+ 31
- 22
react/features/settings/functions.js 파일 보기

@@ -79,14 +79,28 @@ export function normalizeUserInputURL(url: string) {
79 79
 }
80 80
 
81 81
 /**
82
- * Used for web. Returns whether or not only Device Selection is configured to
83
- * display as a setting.
82
+ * Returns the properties for the "More" tab from settings dialog from Redux
83
+ * state.
84 84
  *
85
- * @returns {boolean}
85
+ * @param {(Function|Object)} stateful -The (whole) redux state, or redux's
86
+ * {@code getState} function to be used to retrieve the state.
87
+ * @returns {Object} - The properties for the "More" tab from settings dialog.
86 88
  */
87
-export function shouldShowOnlyDeviceSelection() {
88
-    return interfaceConfig.SETTINGS_SECTIONS.length === 1
89
-        && isSettingEnabled('devices');
89
+export function getMoreTabProps(stateful: Object | Function) {
90
+    const state = toState(stateful);
91
+    const framerate = state['features/screen-share'].captureFrameRate ?? SS_DEFAULT_FRAME_RATE;
92
+    const language = i18next.language || DEFAULT_LANGUAGE;
93
+    const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
94
+
95
+    return {
96
+        currentFramerate: framerate,
97
+        currentLanguage: language,
98
+        desktopShareFramerates: SS_SUPPORTED_FRAMERATES,
99
+        languages: LANGUAGES,
100
+        showLanguageSettings: configuredTabs.includes('language'),
101
+        showPrejoinPage: !state['features/base/settings'].userSelectedSkipPrejoin,
102
+        showPrejoinSettings: state['features/base/config'].prejoinPageEnabled
103
+    };
90 104
 }
91 105
 
92 106
 /**
@@ -97,36 +111,29 @@ export function shouldShowOnlyDeviceSelection() {
97 111
  * {@code getState} function to be used to retrieve the state.
98 112
  * @returns {Object} - The properties for the "More" tab from settings dialog.
99 113
  */
100
-export function getMoreTabProps(stateful: Object | Function) {
114
+export function getModeratorTabProps(stateful: Object | Function) {
101 115
     const state = toState(stateful);
102
-    const framerate = state['features/screen-share'].captureFrameRate ?? SS_DEFAULT_FRAME_RATE;
103
-    const language = i18next.language || DEFAULT_LANGUAGE;
104 116
     const {
105 117
         conference,
106 118
         followMeEnabled,
107 119
         startAudioMutedPolicy,
108
-        startVideoMutedPolicy
120
+        startVideoMutedPolicy,
121
+        startReactionsMuted
109 122
     } = state['features/base/conference'];
110 123
     const followMeActive = isFollowMeActive(state);
111 124
     const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
112 125
 
113
-    // The settings sections to display.
114 126
     const showModeratorSettings = Boolean(
115 127
         conference
116
-            && configuredTabs.includes('moderator')
117
-            && isLocalParticipantModerator(state));
128
+        && configuredTabs.includes('moderator')
129
+        && isLocalParticipantModerator(state));
118 130
 
131
+    // The settings sections to display.
119 132
     return {
120
-        currentFramerate: framerate,
121
-        currentLanguage: language,
122
-        desktopShareFramerates: SS_SUPPORTED_FRAMERATES,
133
+        showModeratorSettings,
123 134
         followMeActive: Boolean(conference && followMeActive),
124 135
         followMeEnabled: Boolean(conference && followMeEnabled),
125
-        languages: LANGUAGES,
126
-        showLanguageSettings: configuredTabs.includes('language'),
127
-        showModeratorSettings,
128
-        showPrejoinSettings: state['features/base/config'].prejoinPageEnabled,
129
-        showPrejoinPage: !state['features/base/settings'].userSelectedSkipPrejoin,
136
+        startReactionsMuted: Boolean(conference && startReactionsMuted),
130 137
         startAudioMuted: Boolean(conference && startAudioMutedPolicy),
131 138
         startVideoMuted: Boolean(conference && startVideoMutedPolicy)
132 139
     };
@@ -178,6 +185,7 @@ export function getSoundsTabProps(stateful: Object | Function) {
178 185
         soundsReactions
179 186
     } = state['features/base/settings'];
180 187
     const enableReactions = isReactionsEnabled(state);
188
+    const moderatorMutedSoundsReactions = state['features/base/conference'].startReactionsMuted ?? false;
181 189
 
182 190
     return {
183 191
         soundsIncomingMessage,
@@ -185,7 +193,8 @@ export function getSoundsTabProps(stateful: Object | Function) {
185 193
         soundsParticipantLeft,
186 194
         soundsTalkWhileMuted,
187 195
         soundsReactions,
188
-        enableReactions
196
+        enableReactions,
197
+        moderatorMutedSoundsReactions
189 198
     };
190 199
 }
191 200
 

+ 2
- 2
react/features/talk-while-muted/middleware.js 파일 보기

@@ -53,8 +53,8 @@ MiddlewareRegistry.register(store => next => action => {
53 53
                     const forceMuted = isForceMuted(local, MEDIA_TYPE.AUDIO, state);
54 54
                     const notification = await dispatch(showNotification({
55 55
                         titleKey: 'toolbar.talkWhileMutedPopup',
56
-                        customActionNameKey: forceMuted ? 'notify.raiseHandAction' : 'notify.unmute',
57
-                        customActionHandler: () => dispatch(forceMuted ? raiseHand(true) : setAudioMuted(false))
56
+                        customActionNameKey: [ forceMuted ? 'notify.raiseHandAction' : 'notify.unmute' ],
57
+                        customActionHandler: [ () => dispatch(forceMuted ? raiseHand(true) : setAudioMuted(false)) ]
58 58
                     }, NOTIFICATION_TIMEOUT_TYPE.LONG));
59 59
 
60 60
                     const { soundsTalkWhileMuted } = getState()['features/base/settings'];

Loading…
취소
저장