Browse Source

fix(av-moderation) Advanced moderation improvements (#9935)

* Update moderation in effect notifications

Only display one notification for each media type. Display notification for keyboard shortcuts as well

* Update muted remotely notification

Display name of moderator in the notification

* Fix indentation on moderation menu

* Update text for video moderation

* Added moderator label in participant pane

* Update microphone icon in participant list

For participants that speak, or are noisy, but aren't dominant speaker, the icon in the participant list will look the same as the dominant speaker icon but will not change their position in the list

* Added sound for asked to unmute notification

* Code review changes

* Code review changes

Use simple var instead of function for audio media state

* Move constants to constants file

* Moved constants from notifications to av-moderation
master
robertpin 3 years ago
parent
commit
ab366b9d94
No account linked to committer's email address

+ 15
- 2
conference.js View File

24
     redirectToStaticPage,
24
     redirectToStaticPage,
25
     reloadWithStoredParams
25
     reloadWithStoredParams
26
 } from './react/features/app/actions';
26
 } from './react/features/app/actions';
27
+import { showModeratedNotification } from './react/features/av-moderation/actions';
28
+import { shouldShowModeratedNotification } from './react/features/av-moderation/functions';
27
 import {
29
 import {
28
     AVATAR_URL_COMMAND,
30
     AVATAR_URL_COMMAND,
29
     EMAIL_COMMAND,
31
     EMAIL_COMMAND,
120
     maybeOpenFeedbackDialog,
122
     maybeOpenFeedbackDialog,
121
     submitFeedback
123
     submitFeedback
122
 } from './react/features/feedback';
124
 } from './react/features/feedback';
123
-import { showNotification } from './react/features/notifications';
125
+import { isModerationNotificationDisplayed, showNotification } from './react/features/notifications';
124
 import { mediaPermissionPromptVisibilityChanged, toggleSlowGUMOverlay } from './react/features/overlay';
126
 import { mediaPermissionPromptVisibilityChanged, toggleSlowGUMOverlay } from './react/features/overlay';
125
 import { suspendDetected } from './react/features/power-monitor';
127
 import { suspendDetected } from './react/features/power-monitor';
126
 import {
128
 import {
871
      * dialogs in case of media permissions error.
873
      * dialogs in case of media permissions error.
872
      */
874
      */
873
     muteAudio(mute, showUI = true) {
875
     muteAudio(mute, showUI = true) {
876
+        const state = APP.store.getState();
877
+
874
         if (!mute
878
         if (!mute
875
-                && isUserInteractionRequiredForUnmute(APP.store.getState())) {
879
+            && isUserInteractionRequiredForUnmute(state)) {
876
             logger.error('Unmuting audio requires user interaction');
880
             logger.error('Unmuting audio requires user interaction');
877
 
881
 
878
             return;
882
             return;
879
         }
883
         }
880
 
884
 
885
+        // check for A/V Moderation when trying to unmute
886
+        if (!mute && shouldShowModeratedNotification(MEDIA_TYPE.AUDIO, state)) {
887
+            if (!isModerationNotificationDisplayed(MEDIA_TYPE.AUDIO, state)) {
888
+                APP.store.dispatch(showModeratedNotification(MEDIA_TYPE.AUDIO));
889
+            }
890
+
891
+            return;
892
+        }
893
+
881
         // Not ready to modify track's state yet
894
         // Not ready to modify track's state yet
882
         if (!this._localTracksInitialized) {
895
         if (!this._localTracksInitialized) {
883
             // This will only modify base/media.audio.muted which is then synced
896
             // This will only modify base/media.audio.muted which is then synced

+ 1
- 0
config.js View File

737
 
737
 
738
     // Array<string> of disabled sounds.
738
     // Array<string> of disabled sounds.
739
     // Possible values:
739
     // Possible values:
740
+    // - 'ASKED_TO_UNMUTE_SOUND'
740
     // - 'E2EE_OFF_SOUND'
741
     // - 'E2EE_OFF_SOUND'
741
     // - 'E2EE_ON_SOUND'
742
     // - 'E2EE_ON_SOUND'
742
     // - 'INCOMING_MSG_SOUND'
743
     // - 'INCOMING_MSG_SOUND'

+ 3
- 3
lang/main.json View File

566
         "moderator": "You're now a moderator",
566
         "moderator": "You're now a moderator",
567
         "muted": "You have started the conversation muted.",
567
         "muted": "You have started the conversation muted.",
568
         "mutedTitle": "You're muted!",
568
         "mutedTitle": "You're muted!",
569
-        "mutedRemotelyTitle": "You've been muted by the moderator",
569
+        "mutedRemotelyTitle": "You've been muted by {{moderator}}",
570
         "mutedRemotelyDescription": "You can always unmute when you're ready to speak. Mute back when you're done to keep noise away from the meeting.",
570
         "mutedRemotelyDescription": "You can always unmute when you're ready to speak. Mute back when you're done to keep noise away from the meeting.",
571
-        "videoMutedRemotelyTitle": "Your camera has been turned off by the moderator",
571
+        "videoMutedRemotelyTitle": "Your camera has been turned off by {{moderator}}",
572
         "videoMutedRemotelyDescription": "You can always turn it on again.",
572
         "videoMutedRemotelyDescription": "You can always turn it on again.",
573
         "passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
573
         "passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
574
         "passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
574
         "passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
623
             "stopEveryonesVideo": "Stop everyone's video",
623
             "stopEveryonesVideo": "Stop everyone's video",
624
             "stopVideo": "Stop video",
624
             "stopVideo": "Stop video",
625
             "unblockEveryoneMicCamera": "Unblock everyone's mic and camera",
625
             "unblockEveryoneMicCamera": "Unblock everyone's mic and camera",
626
-            "videoModeration": "Start video"
626
+            "videoModeration": "Start their video"
627
         }
627
         }
628
     },
628
     },
629
     "passwordSetRemotely": "Set by another participant",
629
     "passwordSetRemotely": "Set by another participant",

+ 12
- 0
react/features/av-moderation/constants.js View File

17
     [MEDIA_TYPE.AUDIO]: 'pendingAudio',
17
     [MEDIA_TYPE.AUDIO]: 'pendingAudio',
18
     [MEDIA_TYPE.VIDEO]: 'pendingVideo'
18
     [MEDIA_TYPE.VIDEO]: 'pendingVideo'
19
 };
19
 };
20
+
21
+export const ASKED_TO_UNMUTE_SOUND_ID = 'ASKED_TO_UNMUTE_SOUND';
22
+
23
+export const AUDIO_MODERATION_NOTIFICATION_ID = 'audio-moderation';
24
+export const VIDEO_MODERATION_NOTIFICATION_ID = 'video-moderation';
25
+export const CS_MODERATION_NOTIFICATION_ID = 'screensharing-moderation';
26
+
27
+export const MODERATION_NOTIFICATIONS = {
28
+    [MEDIA_TYPE.AUDIO]: AUDIO_MODERATION_NOTIFICATION_ID,
29
+    [MEDIA_TYPE.VIDEO]: VIDEO_MODERATION_NOTIFICATION_ID,
30
+    [MEDIA_TYPE.PRESENTER]: CS_MODERATION_NOTIFICATION_ID
31
+};

+ 17
- 4
react/features/av-moderation/middleware.js View File

1
 // @flow
1
 // @flow
2
 import { batch } from 'react-redux';
2
 import { batch } from 'react-redux';
3
 
3
 
4
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
4
 import { getConferenceState } from '../base/conference';
5
 import { getConferenceState } from '../base/conference';
5
 import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
6
 import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
6
 import { MEDIA_TYPE } from '../base/media';
7
 import { MEDIA_TYPE } from '../base/media';
13
     raiseHand
14
     raiseHand
14
 } from '../base/participants';
15
 } from '../base/participants';
15
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
16
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
17
+import { playSound, registerSound, unregisterSound } from '../base/sounds';
16
 import {
18
 import {
17
     hideNotification,
19
     hideNotification,
18
     showNotification
20
     showNotification
35
     participantApproved,
37
     participantApproved,
36
     participantPendingAudio
38
     participantPendingAudio
37
 } from './actions';
39
 } from './actions';
40
+import {
41
+    ASKED_TO_UNMUTE_SOUND_ID, AUDIO_MODERATION_NOTIFICATION_ID,
42
+    CS_MODERATION_NOTIFICATION_ID,
43
+    VIDEO_MODERATION_NOTIFICATION_ID
44
+} from './constants';
38
 import {
45
 import {
39
     isEnabledFromState,
46
     isEnabledFromState,
40
     isParticipantApproved,
47
     isParticipantApproved,
41
     isParticipantPending
48
     isParticipantPending
42
 } from './functions';
49
 } from './functions';
43
-
44
-const VIDEO_MODERATION_NOTIFICATION_ID = 'video-moderation';
45
-const AUDIO_MODERATION_NOTIFICATION_ID = 'audio-moderation';
46
-const CS_MODERATION_NOTIFICATION_ID = 'video-moderation';
50
+import { ASKED_TO_UNMUTE_FILE } from './sounds';
47
 
51
 
48
 MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
52
 MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
49
     const { type } = action;
53
     const { type } = action;
50
     const { conference } = getConferenceState(getState());
54
     const { conference } = getConferenceState(getState());
51
 
55
 
52
     switch (type) {
56
     switch (type) {
57
+    case APP_WILL_MOUNT: {
58
+        dispatch(registerSound(ASKED_TO_UNMUTE_SOUND_ID, ASKED_TO_UNMUTE_FILE));
59
+        break;
60
+    }
61
+    case APP_WILL_UNMOUNT: {
62
+        dispatch(unregisterSound(ASKED_TO_UNMUTE_SOUND_ID));
63
+        break;
64
+    }
53
     case LOCAL_PARTICIPANT_MODERATION_NOTIFICATION: {
65
     case LOCAL_PARTICIPANT_MODERATION_NOTIFICATION: {
54
         let descriptionKey;
66
         let descriptionKey;
55
         let titleKey;
67
         let titleKey;
160
                         customActionNameKey: 'notify.unmute',
172
                         customActionNameKey: 'notify.unmute',
161
                         customActionHandler: () => dispatch(muteLocal(false, MEDIA_TYPE.AUDIO))
173
                         customActionHandler: () => dispatch(muteLocal(false, MEDIA_TYPE.AUDIO))
162
                     }));
174
                     }));
175
+                    dispatch(playSound(ASKED_TO_UNMUTE_SOUND_ID));
163
                 }
176
                 }
164
             });
177
             });
165
 
178
 

+ 6
- 0
react/features/av-moderation/sounds.js View File

1
+/**
2
+ * The name of the bundled audio file which will be played for the raise hand sound.
3
+ *
4
+ * @type {string}
5
+ */
6
+export const ASKED_TO_UNMUTE_FILE = 'asked-unmute.mp3';

+ 4
- 1
react/features/base/media/actions.js View File

4
 
4
 
5
 import { showModeratedNotification } from '../../av-moderation/actions';
5
 import { showModeratedNotification } from '../../av-moderation/actions';
6
 import { shouldShowModeratedNotification } from '../../av-moderation/functions';
6
 import { shouldShowModeratedNotification } from '../../av-moderation/functions';
7
+import { isModerationNotificationDisplayed } from '../../notifications';
7
 
8
 
8
 import {
9
 import {
9
     SET_AUDIO_MUTED,
10
     SET_AUDIO_MUTED,
113
 
114
 
114
         // check for A/V Moderation when trying to unmute
115
         // check for A/V Moderation when trying to unmute
115
         if (!muted && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, state)) {
116
         if (!muted && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, state)) {
116
-            ensureTrack && dispatch(showModeratedNotification(MEDIA_TYPE.VIDEO));
117
+            if (!isModerationNotificationDisplayed(MEDIA_TYPE.VIDEO, state)) {
118
+                ensureTrack && dispatch(showModeratedNotification(MEDIA_TYPE.VIDEO));
119
+            }
117
 
120
 
118
             return;
121
             return;
119
         }
122
         }

+ 5
- 2
react/features/base/participants/actions.js View File

466
  * @returns {Promise}
466
  * @returns {Promise}
467
  */
467
  */
468
 export function participantMutedUs(participant, track) {
468
 export function participantMutedUs(participant, track) {
469
-    return dispatch => {
469
+    return (dispatch, getState) => {
470
         if (!participant) {
470
         if (!participant) {
471
             return;
471
             return;
472
         }
472
         }
474
         const isAudio = track.isAudioTrack();
474
         const isAudio = track.isAudioTrack();
475
 
475
 
476
         dispatch(showNotification({
476
         dispatch(showNotification({
477
-            titleKey: isAudio ? 'notify.mutedRemotelyTitle' : 'notify.videoMutedRemotelyTitle'
477
+            titleKey: isAudio ? 'notify.mutedRemotelyTitle' : 'notify.videoMutedRemotelyTitle',
478
+            titleArguments: {
479
+                moderator: getParticipantDisplayName(getState, participant.getId())
480
+            }
478
         }));
481
         }));
479
     };
482
     };
480
 }
483
 }

+ 4
- 2
react/features/base/tracks/middleware.js View File

3
 import UIEvents from '../../../../service/UI/UIEvents';
3
 import UIEvents from '../../../../service/UI/UIEvents';
4
 import { showModeratedNotification } from '../../av-moderation/actions';
4
 import { showModeratedNotification } from '../../av-moderation/actions';
5
 import { shouldShowModeratedNotification } from '../../av-moderation/functions';
5
 import { shouldShowModeratedNotification } from '../../av-moderation/functions';
6
-import { hideNotification } from '../../notifications';
6
+import { hideNotification, isModerationNotificationDisplayed } from '../../notifications';
7
 import { isPrejoinPageVisible } from '../../prejoin/functions';
7
 import { isPrejoinPageVisible } from '../../prejoin/functions';
8
 import { getAvailableDevices } from '../devices/actions';
8
 import { getAvailableDevices } from '../devices/actions';
9
 import {
9
 import {
142
             // check for A/V Moderation when trying to start screen sharing
142
             // check for A/V Moderation when trying to start screen sharing
143
             if ((action.enabled || action.enabled === undefined)
143
             if ((action.enabled || action.enabled === undefined)
144
                 && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, store.getState())) {
144
                 && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, store.getState())) {
145
-                store.dispatch(showModeratedNotification(MEDIA_TYPE.PRESENTER));
145
+                if (!isModerationNotificationDisplayed(MEDIA_TYPE.PRESENTER, store.getState())) {
146
+                    store.dispatch(showModeratedNotification(MEDIA_TYPE.PRESENTER));
147
+                }
146
 
148
 
147
                 return;
149
                 return;
148
             }
150
             }

+ 17
- 0
react/features/notifications/functions.js View File

1
 // @flow
1
 // @flow
2
 
2
 
3
+import { MODERATION_NOTIFICATIONS } from '../av-moderation/constants';
4
+import { MEDIA_TYPE } from '../base/media';
3
 import { toState } from '../base/redux';
5
 import { toState } from '../base/redux';
4
 
6
 
5
 declare var interfaceConfig: Object;
7
 declare var interfaceConfig: Object;
26
 export function joinLeaveNotificationsDisabled() {
28
 export function joinLeaveNotificationsDisabled() {
27
     return Boolean(typeof interfaceConfig !== 'undefined' && interfaceConfig?.DISABLE_JOIN_LEAVE_NOTIFICATIONS);
29
     return Boolean(typeof interfaceConfig !== 'undefined' && interfaceConfig?.DISABLE_JOIN_LEAVE_NOTIFICATIONS);
28
 }
30
 }
31
+
32
+/**
33
+ * Returns whether or not the moderation notification for the given type is displayed.
34
+ *
35
+ * @param {MEDIA_TYPE} mediaType - The media type to check.
36
+ * @param {Object | Function} stateful - The redux store state.
37
+ * @returns {boolean}
38
+ */
39
+export function isModerationNotificationDisplayed(mediaType: MEDIA_TYPE, stateful: Object | Function) {
40
+    const state = toState(stateful);
41
+
42
+    const { notifications } = state['features/notifications'];
43
+
44
+    return Boolean(notifications.find(n => n.uid === MODERATION_NOTIFICATIONS[mediaType]));
45
+}

+ 2
- 2
react/features/participants-pane/components/FooterContextMenu.js View File

55
         },
55
         },
56
         text: {
56
         text: {
57
             color: '#C2C2C2',
57
             color: '#C2C2C2',
58
-            padding: '10px 16px 10px 52px'
58
+            padding: '10px 16px'
59
         },
59
         },
60
         paddedAction: {
60
         paddedAction: {
61
-            marginLeft: '36px;'
61
+            marginLeft: '36px'
62
         }
62
         }
63
     };
63
     };
64
 });
64
 });

+ 59
- 6
react/features/participants-pane/components/web/MeetingParticipantItem.js View File

1
 // @flow
1
 // @flow
2
 
2
 
3
-import React from 'react';
3
+import React, { useCallback, useEffect, useState } from 'react';
4
 
4
 
5
+import { JitsiTrackEvents } from '../../../base/lib-jitsi-meet';
6
+import { MEDIA_TYPE } from '../../../base/media';
5
 import {
7
 import {
6
     getLocalParticipant,
8
     getLocalParticipant,
7
     getParticipantByIdOrUndefined,
9
     getParticipantByIdOrUndefined,
8
-    getParticipantDisplayName
10
+    getParticipantDisplayName,
11
+    isParticipantModerator
9
 } from '../../../base/participants';
12
 } from '../../../base/participants';
10
 import { connect } from '../../../base/redux';
13
 import { connect } from '../../../base/redux';
11
-import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
12
-import { ACTION_TRIGGER, type MediaState } from '../../constants';
14
+import {
15
+    getLocalAudioTrack,
16
+    getTrackByMediaTypeAndParticipant,
17
+    isParticipantAudioMuted,
18
+    isParticipantVideoMuted
19
+} from '../../../base/tracks';
20
+import { ACTION_TRIGGER, type MediaState, MEDIA_STATE } from '../../constants';
13
 import {
21
 import {
14
     getParticipantAudioMediaState,
22
     getParticipantAudioMediaState,
15
     getParticipantVideoMediaState,
23
     getParticipantVideoMediaState,
27
      */
35
      */
28
     _audioMediaState: MediaState,
36
     _audioMediaState: MediaState,
29
 
37
 
38
+    /**
39
+     * The audio track related to the participant.
40
+     */
41
+    _audioTrack: ?Object,
42
+
30
     /**
43
     /**
31
      * Media state for video.
44
      * Media state for video.
32
      */
45
      */
136
  */
149
  */
137
 function MeetingParticipantItem({
150
 function MeetingParticipantItem({
138
     _audioMediaState,
151
     _audioMediaState,
152
+    _audioTrack,
139
     _videoMediaState,
153
     _videoMediaState,
140
     _displayName,
154
     _displayName,
141
     _local,
155
     _local,
155
     participantActionEllipsisLabel,
169
     participantActionEllipsisLabel,
156
     youText
170
     youText
157
 }: Props) {
171
 }: Props) {
172
+
173
+    const [ hasAudioLevels, setHasAudioLevel ] = useState(false);
174
+    const [ registeredEvent, setRegisteredEvent ] = useState(false);
175
+
176
+    const _updateAudioLevel = useCallback(level => {
177
+        const audioLevel = typeof level === 'number' && !isNaN(level)
178
+            ? level : 0;
179
+
180
+        setHasAudioLevel(audioLevel > 0.009);
181
+    }, []);
182
+
183
+    useEffect(() => {
184
+        if (_audioTrack && !registeredEvent) {
185
+            const { jitsiTrack } = _audioTrack;
186
+
187
+            if (jitsiTrack) {
188
+                jitsiTrack.on(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, _updateAudioLevel);
189
+                setRegisteredEvent(true);
190
+            }
191
+        }
192
+
193
+        return () => {
194
+            if (_audioTrack && registeredEvent) {
195
+                const { jitsiTrack } = _audioTrack;
196
+
197
+                jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, _updateAudioLevel);
198
+            }
199
+        };
200
+    }, [ _audioTrack ]);
201
+
202
+    const audioMediaState = _audioMediaState === MEDIA_STATE.UNMUTED && hasAudioLevels
203
+        ? MEDIA_STATE.DOMINANT_SPEAKER : _audioMediaState;
204
+
158
     return (
205
     return (
159
         <ParticipantItem
206
         <ParticipantItem
160
             actionsTrigger = { ACTION_TRIGGER.HOVER }
207
             actionsTrigger = { ACTION_TRIGGER.HOVER }
161
-            audioMediaState = { _audioMediaState }
208
+            audioMediaState = { audioMediaState }
162
             displayName = { _displayName }
209
             displayName = { _displayName }
163
             isHighlighted = { isHighlighted }
210
             isHighlighted = { isHighlighted }
211
+            isModerator = { isParticipantModerator(_participant) }
164
             local = { _local }
212
             local = { _local }
165
             onLeave = { onLeave }
213
             onLeave = { onLeave }
166
             openDrawerForParticipant = { openDrawerForParticipant }
214
             openDrawerForParticipant = { openDrawerForParticipant }
181
                     <ParticipantActionEllipsis
229
                     <ParticipantActionEllipsis
182
                         aria-label = { participantActionEllipsisLabel }
230
                         aria-label = { participantActionEllipsisLabel }
183
                         onClick = { onContextMenu } />
231
                         onClick = { onContextMenu } />
184
-                 </>
232
+                </>
185
             }
233
             }
186
 
234
 
187
             {!overflowDrawer && _localVideoOwner && _participant?.isFakeParticipant && (
235
             {!overflowDrawer && _localVideoOwner && _participant?.isFakeParticipant && (
214
     const _videoMediaState = getParticipantVideoMediaState(participant, _isVideoMuted, state);
262
     const _videoMediaState = getParticipantVideoMediaState(participant, _isVideoMuted, state);
215
     const _quickActionButtonType = getQuickActionButtonType(participant, _isAudioMuted, state);
263
     const _quickActionButtonType = getQuickActionButtonType(participant, _isAudioMuted, state);
216
 
264
 
265
+    const tracks = state['features/base/tracks'];
266
+    const _audioTrack = participantID === localParticipantId
267
+        ? getLocalAudioTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantID);
268
+
217
     return {
269
     return {
218
         _audioMediaState,
270
         _audioMediaState,
271
+        _audioTrack,
219
         _videoMediaState,
272
         _videoMediaState,
220
         _displayName: getParticipantDisplayName(state, participant?.id),
273
         _displayName: getParticipantDisplayName(state, participant?.id),
221
         _local: Boolean(participant?.local),
274
         _local: Boolean(participant?.local),

+ 29
- 7
react/features/participants-pane/components/web/ParticipantItem.js View File

3
 import React, { type Node, useCallback } from 'react';
3
 import React, { type Node, useCallback } from 'react';
4
 
4
 
5
 import { Avatar } from '../../../base/avatar';
5
 import { Avatar } from '../../../base/avatar';
6
+import { translate } from '../../../base/i18n';
6
 import {
7
 import {
7
     ACTION_TRIGGER,
8
     ACTION_TRIGGER,
8
     AudioStateIcons,
9
     AudioStateIcons,
14
 
15
 
15
 import { RaisedHandIndicator } from './RaisedHandIndicator';
16
 import { RaisedHandIndicator } from './RaisedHandIndicator';
16
 import {
17
 import {
18
+    ModeratorLabel,
17
     ParticipantActionsHover,
19
     ParticipantActionsHover,
18
     ParticipantActionsPermanent,
20
     ParticipantActionsPermanent,
19
     ParticipantContainer,
21
     ParticipantContainer,
20
     ParticipantContent,
22
     ParticipantContent,
23
+    ParticipantDetailsContainer,
21
     ParticipantName,
24
     ParticipantName,
22
     ParticipantNameContainer,
25
     ParticipantNameContainer,
23
     ParticipantStates
26
     ParticipantStates
58
      */
61
      */
59
     isHighlighted?: boolean,
62
     isHighlighted?: boolean,
60
 
63
 
64
+    /**
65
+     * Whether or not the participant is a moderator.
66
+     */
67
+    isModerator: boolean,
68
+
61
     /**
69
     /**
62
      * True if the participant is local.
70
      * True if the participant is local.
63
      */
71
      */
93
      */
101
      */
94
     videoMediaState: MediaState,
102
     videoMediaState: MediaState,
95
 
103
 
104
+    /**
105
+     * Invoked to obtain translated strings.
106
+     */
107
+    t: Function,
108
+
96
     /**
109
     /**
97
      * The translated "you" text.
110
      * The translated "you" text.
98
      */
111
      */
105
  * @param {Props} props - The props of the component.
118
  * @param {Props} props - The props of the component.
106
  * @returns {ReactNode}
119
  * @returns {ReactNode}
107
  */
120
  */
108
-export default function ParticipantItem({
121
+function ParticipantItem({
109
     children,
122
     children,
110
     isHighlighted,
123
     isHighlighted,
124
+    isModerator,
111
     onLeave,
125
     onLeave,
112
     actionsTrigger = ACTION_TRIGGER.HOVER,
126
     actionsTrigger = ACTION_TRIGGER.HOVER,
113
     audioMediaState = MEDIA_STATE.NONE,
127
     audioMediaState = MEDIA_STATE.NONE,
118
     openDrawerForParticipant,
132
     openDrawerForParticipant,
119
     overflowDrawer,
133
     overflowDrawer,
120
     raisedHand,
134
     raisedHand,
135
+    t,
121
     youText
136
     youText
122
 }: Props) {
137
 }: Props) {
123
     const ParticipantActions = Actions[actionsTrigger];
138
     const ParticipantActions = Actions[actionsTrigger];
140
                 participantId = { participantID }
155
                 participantId = { participantID }
141
                 size = { 32 } />
156
                 size = { 32 } />
142
             <ParticipantContent>
157
             <ParticipantContent>
143
-                <ParticipantNameContainer>
144
-                    <ParticipantName>
145
-                        { displayName }
146
-                    </ParticipantName>
147
-                    { local ? <span>&nbsp;({ youText })</span> : null }
148
-                </ParticipantNameContainer>
158
+                <ParticipantDetailsContainer>
159
+                    <ParticipantNameContainer>
160
+                        <ParticipantName>
161
+                            { displayName }
162
+                        </ParticipantName>
163
+                        { local ? <span>&nbsp;({ youText })</span> : null }
164
+                    </ParticipantNameContainer>
165
+                    {isModerator && <ModeratorLabel>
166
+                        {t('videothumbnail.moderator')}
167
+                    </ModeratorLabel>}
168
+                </ParticipantDetailsContainer>
149
                 { !local && <ParticipantActions children = { children } /> }
169
                 { !local && <ParticipantActions children = { children } /> }
150
                 <ParticipantStates>
170
                 <ParticipantStates>
151
                     { raisedHand && <RaisedHandIndicator /> }
171
                     { raisedHand && <RaisedHandIndicator /> }
156
         </ParticipantContainer>
176
         </ParticipantContainer>
157
     );
177
     );
158
 }
178
 }
179
+
180
+export default translate(ParticipantItem);

+ 15
- 0
react/features/participants-pane/components/web/styled.js View File

282
   color: white;
282
   color: white;
283
   display: flex;
283
   display: flex;
284
   font-size: 13px;
284
   font-size: 13px;
285
+  font-weight: normal;
285
   height: ${props => props.theme.participantItemHeight}px;
286
   height: ${props => props.theme.participantItemHeight}px;
286
   margin: 0 -${props => props.theme.panePadding}px;
287
   margin: 0 -${props => props.theme.panePadding}px;
287
   padding-left: ${props => props.theme.panePadding}px;
288
   padding-left: ${props => props.theme.panePadding}px;
341
 `;
342
 `;
342
 
343
 
343
 export const ParticipantNameContainer = styled.div`
344
 export const ParticipantNameContainer = styled.div`
345
+  display: flex;
346
+  flex: 1;
347
+  overflow: hidden;
348
+`;
349
+
350
+export const ModeratorLabel = styled.div`
351
+  font-size: 12px;
352
+  line-height: 16px;
353
+  color: #858585;
354
+`;
355
+
356
+export const ParticipantDetailsContainer = styled.div`
344
   display: flex;
357
   display: flex;
345
   flex: 1;
358
   flex: 1;
346
   margin-right: 8px;
359
   margin-right: 8px;
347
   overflow: hidden;
360
   overflow: hidden;
361
+  flex-direction: column;
362
+  justify-content: flex-start;
348
 `;
363
 `;
349
 
364
 
350
 export const RaisedHandIndicatorBackground = styled.div`
365
 export const RaisedHandIndicatorBackground = styled.div`

+ 4
- 1
react/features/video-menu/actions.any.js View File

23
     getRemoteParticipants,
23
     getRemoteParticipants,
24
     muteRemoteParticipant
24
     muteRemoteParticipant
25
 } from '../base/participants';
25
 } from '../base/participants';
26
+import { isModerationNotificationDisplayed } from '../notifications';
26
 
27
 
27
 declare var APP: Object;
28
 declare var APP: Object;
28
 
29
 
47
 
48
 
48
         // check for A/V Moderation when trying to unmute
49
         // check for A/V Moderation when trying to unmute
49
         if (!enable && shouldShowModeratedNotification(MEDIA_TYPE.AUDIO, getState())) {
50
         if (!enable && shouldShowModeratedNotification(MEDIA_TYPE.AUDIO, getState())) {
50
-            dispatch(showModeratedNotification(MEDIA_TYPE.AUDIO));
51
+            if (!isModerationNotificationDisplayed(MEDIA_TYPE.AUDIO, getState())) {
52
+                dispatch(showModeratedNotification(MEDIA_TYPE.AUDIO));
53
+            }
51
 
54
 
52
             return;
55
             return;
53
         }
56
         }

BIN
sounds/asked-unmute.mp3 View File


Loading…
Cancel
Save