浏览代码

feat(mute): mute everyone / everyone else

master
Gabriel Imre 5 年前
父节点
当前提交
24a1a60f04

+ 1
- 1
interface_config.js 查看文件

@@ -51,7 +51,7 @@ var interfaceConfig = {
51 51
         'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
52 52
         'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
53 53
         'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
54
-        'tileview', 'videobackgroundblur', 'download', 'help'
54
+        'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone'
55 55
     ],
56 56
 
57 57
     SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],

+ 9
- 0
lang/main.json 查看文件

@@ -209,6 +209,12 @@
209 209
         "micNotSendingDataTitle": "Your mic is muted by your system settings",
210 210
         "micPermissionDeniedError": "You have not granted permission to use your microphone. You can still join the conference but others won't hear you. Use the camera button in the address bar to fix this.",
211 211
         "micUnknownError": "Cannot use microphone for an unknown reason.",
212
+        "muteEveryoneElseDialog": "Once muted, you won't be able to unmute them, but they can unmute themselves at any time.",
213
+        "muteEveryoneElseTitle": "Mute everyone except {{whom}}?",
214
+        "muteEveryoneDialog": "Are you sure you want to mute everyone? You won't be able to unmute them, but they can unmute themselves at any time.",
215
+        "muteEveryoneTitle": "Mute everyone?",
216
+        "muteEveryoneSelf": "yourself",
217
+        "muteEveryoneStartMuted": "Everyone starts muted from now on",
212 218
         "muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
213 219
         "muteParticipantButton": "Mute",
214 220
         "muteParticipantDialog": "Are you sure you want to mute this participant? You won't be able to unmute them, but they can unmute themselves at any time.",
@@ -592,6 +598,7 @@
592 598
             "moreActionsMenu": "More actions menu",
593 599
             "moreOptions": "Show more options",
594 600
             "mute": "Toggle mute audio",
601
+            "muteEveryone": "Mute everyone",
595 602
             "pip": "Toggle Picture-in-Picture mode",
596 603
             "privateMessage": "Send private message",
597 604
             "profile": "Edit your profile",
@@ -635,6 +642,7 @@
635 642
         "moreActions": "More actions",
636 643
         "moreOptions": "More options",
637 644
         "mute": "Mute / Unmute",
645
+        "muteEveryone": "Mute everyone",
638 646
         "noAudioSignalTitle": "There is no input coming from your mic!",
639 647
         "noAudioSignalDesc": "If you did not purposely mute it from system settings or hardware, consider switching the device.",
640 648
         "noAudioSignalDescSuggestion": "If you did not purposely mute it from system settings or hardware, consider switching to the suggested device.",
@@ -722,6 +730,7 @@
722 730
     },
723 731
     "videothumbnail": {
724 732
         "domute": "Mute",
733
+        "domuteOthers": "Mute everyone else",
725 734
         "flip": "Flip",
726 735
         "kick": "Kick out",
727 736
         "moderator": "Moderator",

+ 68
- 0
react/features/remote-video-menu/actions.js 查看文件

@@ -1,9 +1,24 @@
1 1
 // @flow
2
+import type { Dispatch } from 'redux';
2 3
 
4
+import {
5
+    AUDIO_MUTE,
6
+    createRemoteMuteConfirmedEvent,
7
+    createToolbarEvent,
8
+    sendAnalytics
9
+} from '../analytics';
3 10
 import { hideDialog } from '../base/dialog';
11
+import {
12
+    getLocalParticipant,
13
+    muteRemoteParticipant
14
+} from '../base/participants';
15
+import { setAudioMuted } from '../base/media';
16
+import UIEvents from '../../../service/UI/UIEvents';
4 17
 
5 18
 import { RemoteVideoMenu } from './components';
6 19
 
20
+declare var APP: Object;
21
+
7 22
 /**
8 23
  * Hides the remote video menu.
9 24
  *
@@ -12,3 +27,56 @@ import { RemoteVideoMenu } from './components';
12 27
 export function hideRemoteVideoMenu() {
13 28
     return hideDialog(RemoteVideoMenu);
14 29
 }
30
+
31
+/**
32
+ * Mutes the local participant.
33
+ *
34
+ * @param {boolean} enable - Whether to mute or unmute.
35
+ * @returns {Function}
36
+ */
37
+export function muteLocal(enable: boolean) {
38
+    return (dispatch: Dispatch<any>) => {
39
+        sendAnalytics(createToolbarEvent(AUDIO_MUTE, { enable }));
40
+        dispatch(setAudioMuted(enable, /* ensureTrack */ true));
41
+
42
+        // FIXME: The old conference logic as well as the shared video feature
43
+        // still rely on this event being emitted.
44
+        typeof APP === 'undefined'
45
+            || APP.UI.emitEvent(UIEvents.AUDIO_MUTED, enable, true);
46
+    };
47
+}
48
+
49
+/**
50
+ * Mutes the remote participant with the given ID.
51
+ *
52
+ * @param {string} participantId - ID of the participant to mute.
53
+ * @returns {Function}
54
+ */
55
+export function muteRemote(participantId: string) {
56
+    return (dispatch: Dispatch<any>) => {
57
+        sendAnalytics(createRemoteMuteConfirmedEvent(participantId));
58
+        dispatch(muteRemoteParticipant(participantId));
59
+    };
60
+}
61
+
62
+/**
63
+ * Mutes all participants.
64
+ *
65
+ * @param {Array<string>} exclude - Array of participant IDs to not mute.
66
+ * @returns {Function}
67
+ */
68
+export function muteAllParticipants(exclude: Array<string>) {
69
+    return (dispatch: Dispatch<any>, getState: Function) => {
70
+        const state = getState();
71
+        const localId = getLocalParticipant(state).id;
72
+        const participantIds = state['features/base/participants']
73
+            .map(p => p.id);
74
+
75
+        /* eslint-disable no-confusing-arrow */
76
+        participantIds
77
+            .filter(id => !exclude.includes(id))
78
+            .map(id => id === localId ? muteLocal(true) : muteRemote(id))
79
+            .map(dispatch);
80
+        /* eslint-enable no-confusing-arrow */
81
+    };
82
+}

+ 6
- 12
react/features/remote-video-menu/components/AbstractMuteRemoteParticipantDialog.js 查看文件

@@ -2,17 +2,13 @@
2 2
 
3 3
 import { Component } from 'react';
4 4
 
5
-import {
6
-    createRemoteMuteConfirmedEvent,
7
-    sendAnalytics
8
-} from '../../analytics';
9
-import { muteRemoteParticipant } from '../../base/participants';
5
+import { muteRemote } from '../actions';
10 6
 
11 7
 /**
12 8
  * The type of the React {@code Component} props of
13 9
  * {@link AbstractMuteRemoteParticipantDialog}.
14 10
  */
15
-type Props = {
11
+export type Props = {
16 12
 
17 13
     /**
18 14
      * The Redux dispatch function.
@@ -35,15 +31,15 @@ type Props = {
35 31
  *
36 32
  * @extends Component
37 33
  */
38
-export default class AbstractMuteRemoteParticipantDialog
39
-    extends Component<Props> {
34
+export default class AbstractMuteRemoteParticipantDialog<P:Props = Props>
35
+    extends Component<P> {
40 36
     /**
41 37
      * Initializes a new {@code AbstractMuteRemoteParticipantDialog} instance.
42 38
      *
43 39
      * @param {Object} props - The read-only properties with which the new
44 40
      * instance is to be initialized.
45 41
      */
46
-    constructor(props: Props) {
42
+    constructor(props: P) {
47 43
         super(props);
48 44
 
49 45
         // Bind event handlers so they are only bound once per instance.
@@ -61,9 +57,7 @@ export default class AbstractMuteRemoteParticipantDialog
61 57
     _onSubmit() {
62 58
         const { dispatch, participantID } = this.props;
63 59
 
64
-        sendAnalytics(createRemoteMuteConfirmedEvent(participantID));
65
-
66
-        dispatch(muteRemoteParticipant(participantID));
60
+        dispatch(muteRemote(participantID));
67 61
 
68 62
         return true;
69 63
     }

+ 123
- 0
react/features/remote-video-menu/components/web/MuteEveryoneDialog.js 查看文件

@@ -0,0 +1,123 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { Dialog } from '../../../base/dialog';
6
+import { translate } from '../../../base/i18n';
7
+import { connect } from '../../../base/redux';
8
+
9
+import AbstractMuteRemoteParticipantDialog, {
10
+    type Props as AbstractProps
11
+} from '../AbstractMuteRemoteParticipantDialog';
12
+import { muteAllParticipants } from '../../actions';
13
+
14
+declare var APP: Object;
15
+
16
+/**
17
+ * The type of the React {@code Component} props of
18
+ * {@link MuteEveryoneDialog}.
19
+ */
20
+type Props = AbstractProps & {
21
+
22
+    /**
23
+     * The IDs of the remote participants to exclude from being muted.
24
+     */
25
+    exclude: Array<string>
26
+};
27
+
28
+/**
29
+ * Translations needed for dialog rendering.
30
+ */
31
+type Translations = {
32
+
33
+    /**
34
+     * Content text.
35
+     */
36
+    content: string,
37
+
38
+    /**
39
+     * Title text.
40
+     */
41
+    title: string
42
+}
43
+
44
+/**
45
+ * A React Component with the contents for a dialog that asks for confirmation
46
+ * from the user before muting a remote participant.
47
+ *
48
+ * @extends Component
49
+ */
50
+class MuteEveryoneDialog extends AbstractMuteRemoteParticipantDialog<Props> {
51
+    static defaultProps = {
52
+        exclude: [],
53
+        muteLocal: false
54
+    };
55
+
56
+    /**
57
+     * Implements React's {@link Component#render()}.
58
+     *
59
+     * @inheritdoc
60
+     * @returns {ReactElement}
61
+     */
62
+    render() {
63
+        const { content, title } = this._getTranslations();
64
+
65
+        return (
66
+            <Dialog
67
+                okKey = 'dialog.muteParticipantButton'
68
+                onSubmit = { this._onSubmit }
69
+                titleString = { title }
70
+                width = 'small'>
71
+                <div>
72
+                    { content }
73
+                </div>
74
+            </Dialog>
75
+        );
76
+    }
77
+
78
+    _onSubmit: () => boolean;
79
+
80
+    /**
81
+     * Callback to be invoked when the value of this dialog is submitted.
82
+     *
83
+     * @returns {boolean}
84
+     */
85
+    _onSubmit() {
86
+        const {
87
+            dispatch,
88
+            exclude
89
+        } = this.props;
90
+
91
+        dispatch(muteAllParticipants(exclude));
92
+
93
+        return true;
94
+    }
95
+
96
+    /**
97
+     * Method to get translations depending on whether we have an exclusive
98
+     * mute or not.
99
+     *
100
+     * @returns {Translations}
101
+     * @private
102
+     */
103
+    _getTranslations(): Translations {
104
+        const { exclude, t } = this.props;
105
+        const { conference } = APP;
106
+        const whom = exclude
107
+            // eslint-disable-next-line no-confusing-arrow
108
+            .map(id => conference.isLocalId(id)
109
+                ? t('dialog.muteEveryoneSelf')
110
+                : conference.getParticipantDisplayName(id))
111
+            .join(', ');
112
+
113
+        return whom.length ? {
114
+            content: t('dialog.muteEveryoneElseDialog'),
115
+            title: t('dialog.muteEveryoneElseTitle', { whom })
116
+        } : {
117
+            content: t('dialog.muteEveryoneDialog'),
118
+            title: t('dialog.muteEveryoneTitle')
119
+        };
120
+    }
121
+}
122
+
123
+export default translate(connect()(MuteEveryoneDialog));

+ 71
- 0
react/features/remote-video-menu/components/web/MuteEveryoneElseButton.js 查看文件

@@ -0,0 +1,71 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { createToolbarEvent, sendAnalytics } from '../../../analytics';
6
+import { openDialog } from '../../../base/dialog';
7
+import { translate } from '../../../base/i18n';
8
+import { IconMicDisabled } from '../../../base/icons';
9
+import { connect } from '../../../base/redux';
10
+
11
+import AbstractMuteButton, {
12
+    _mapStateToProps,
13
+    type Props
14
+} from '../AbstractMuteButton';
15
+import MuteEveryoneDialog from './MuteEveryoneDialog';
16
+import RemoteVideoMenuButton from './RemoteVideoMenuButton';
17
+
18
+/**
19
+ * Implements a React {@link Component} which displays a button for audio muting
20
+ * every participant in the conference except the one with the given
21
+ * participantID
22
+ */
23
+class MuteEveryoneElseButton extends AbstractMuteButton {
24
+    /**
25
+     * Instantiates a new {@code MuteEveryoneElseButton}.
26
+     *
27
+     * @inheritdoc
28
+     */
29
+    constructor(props: Props) {
30
+        super(props);
31
+
32
+        this._handleClick = this._handleClick.bind(this);
33
+    }
34
+
35
+    /**
36
+     * Implements React's {@link Component#render()}.
37
+     *
38
+     * @inheritdoc
39
+     * @returns {ReactElement}
40
+     */
41
+    render() {
42
+        const { participantID, t } = this.props;
43
+
44
+        return (
45
+            <RemoteVideoMenuButton
46
+                buttonText = { t('videothumbnail.domuteOthers') }
47
+                displayClass = { 'mutelink' }
48
+                icon = { IconMicDisabled }
49
+                id = { `mutelink_${participantID}` }
50
+                // eslint-disable-next-line react/jsx-handler-names
51
+                onClick = { this._handleClick } />
52
+        );
53
+    }
54
+
55
+    _handleClick: () => void;
56
+
57
+    /**
58
+     * Handles clicking / pressing the button, and opens a confirmation dialog.
59
+     *
60
+     * @private
61
+     * @returns {void}
62
+     */
63
+    _handleClick() {
64
+        const { dispatch, participantID } = this.props;
65
+
66
+        sendAnalytics(createToolbarEvent('mute.everyoneelse.pressed'));
67
+        dispatch(openDialog(MuteEveryoneDialog, { exclude: [ participantID ] }));
68
+    }
69
+}
70
+
71
+export default translate(connect(_mapStateToProps)(MuteEveryoneElseButton));

+ 6
- 0
react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js 查看文件

@@ -9,6 +9,7 @@ import { connect } from '../../../base/redux';
9 9
 
10 10
 import {
11 11
     MuteButton,
12
+    MuteEveryoneElseButton,
12 13
     KickButton,
13 14
     PrivateMessageMenuButton,
14 15
     RemoteControlButton,
@@ -174,6 +175,11 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
174 175
                     key = 'mute'
175 176
                     participantID = { participantID } />
176 177
             );
178
+            buttons.push(
179
+                <MuteEveryoneElseButton
180
+                    key = 'mute-others'
181
+                    participantID = { participantID } />
182
+            );
177 183
             buttons.push(
178 184
                 <KickButton
179 185
                     key = 'kick'

+ 2
- 0
react/features/remote-video-menu/components/web/index.js 查看文件

@@ -5,6 +5,8 @@ export {
5 5
     default as KickRemoteParticipantDialog
6 6
 } from './KickRemoteParticipantDialog';
7 7
 export { default as MuteButton } from './MuteButton';
8
+export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton';
9
+export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
8 10
 export {
9 11
     default as MuteRemoteParticipantDialog
10 12
 } from './MuteRemoteParticipantDialog';

+ 3
- 10
react/features/toolbox/components/AudioMuteButton.js 查看文件

@@ -4,16 +4,15 @@ import {
4 4
     ACTION_SHORTCUT_TRIGGERED,
5 5
     AUDIO_MUTE,
6 6
     createShortcutEvent,
7
-    createToolbarEvent,
8 7
     sendAnalytics
9 8
 } from '../../analytics';
10 9
 import { translate } from '../../base/i18n';
11
-import { MEDIA_TYPE, setAudioMuted } from '../../base/media';
10
+import { MEDIA_TYPE } from '../../base/media';
12 11
 import { connect } from '../../base/redux';
13 12
 import { AbstractAudioMuteButton } from '../../base/toolbox';
14 13
 import type { AbstractButtonProps } from '../../base/toolbox';
15 14
 import { isLocalTrackMuted } from '../../base/tracks';
16
-import UIEvents from '../../../../service/UI/UIEvents';
15
+import { muteLocal } from '../../remote-video-menu/actions';
17 16
 
18 17
 declare var APP: Object;
19 18
 
@@ -125,13 +124,7 @@ class AudioMuteButton extends AbstractAudioMuteButton<Props, *> {
125 124
      * @returns {void}
126 125
      */
127 126
     _setAudioMuted(audioMuted: boolean) {
128
-        sendAnalytics(createToolbarEvent(AUDIO_MUTE, { enable: audioMuted }));
129
-        this.props.dispatch(setAudioMuted(audioMuted, /* ensureTrack */ true));
130
-
131
-        // FIXME: The old conference logic as well as the shared video feature
132
-        // still rely on this event being emitted.
133
-        typeof APP === 'undefined'
134
-            || APP.UI.emitEvent(UIEvents.AUDIO_MUTED, audioMuted, true);
127
+        this.props.dispatch(muteLocal(audioMuted));
135 128
     }
136 129
 
137 130
     /**

+ 75
- 0
react/features/toolbox/components/web/MuteEveryoneButton.js 查看文件

@@ -0,0 +1,75 @@
1
+// @flow
2
+
3
+import { createToolbarEvent, sendAnalytics } from '../../../analytics';
4
+import { openDialog } from '../../../base/dialog';
5
+import { translate } from '../../../base/i18n';
6
+import { IconMicDisabled } from '../../../base/icons';
7
+import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participants';
8
+import { connect } from '../../../base/redux';
9
+import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox';
10
+import { MuteEveryoneDialog } from '../../../remote-video-menu';
11
+
12
+type Props = AbstractButtonProps & {
13
+
14
+    /**
15
+     * The Redux dispatch function.
16
+     */
17
+    dispatch: Function,
18
+
19
+    /*
20
+     ** Whether the local participant is a moderator or not.
21
+     */
22
+    isModerator: Boolean,
23
+
24
+    /**
25
+     * The ID of the local participant.
26
+     */
27
+    localParticipantId: string
28
+};
29
+
30
+/**
31
+ * Implements a React {@link Component} which displays a button for audio muting
32
+ * every participant (except the local one)
33
+ */
34
+class MuteEveryoneButton extends AbstractButton<Props, *> {
35
+    accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryone';
36
+    icon = IconMicDisabled;
37
+    label = 'toolbar.muteEveryone';
38
+    tooltip = 'toolbar.muteEveryone';
39
+
40
+    /**
41
+     * Handles clicking / pressing the button, and opens a confirmation dialog.
42
+     *
43
+     * @private
44
+     * @returns {void}
45
+     */
46
+    _handleClick() {
47
+        const { dispatch, localParticipantId } = this.props;
48
+
49
+        sendAnalytics(createToolbarEvent('mute.everyone.pressed'));
50
+        dispatch(openDialog(MuteEveryoneDialog, {
51
+            exclude: [ localParticipantId ]
52
+        }));
53
+    }
54
+}
55
+
56
+/**
57
+ * Maps part of the redux state to the component's props.
58
+ *
59
+ * @param {Object} state - The redux store/state.
60
+ * @param {Props} ownProps - The component's own props.
61
+ * @returns {Object}
62
+ */
63
+function _mapStateToProps(state: Object, ownProps: Props) {
64
+    const localParticipant = getLocalParticipant(state);
65
+    const isModerator = localParticipant.role === PARTICIPANT_ROLE.MODERATOR;
66
+    const { visible } = ownProps;
67
+
68
+    return {
69
+        isModerator,
70
+        localParticipantId: localParticipant.id,
71
+        visible: visible && isModerator
72
+    };
73
+}
74
+
75
+export default translate(connect(_mapStateToProps)(MuteEveryoneButton));

+ 5
- 0
react/features/toolbox/components/web/Toolbox.js 查看文件

@@ -78,6 +78,7 @@ import HangupButton from '../HangupButton';
78 78
 import HelpButton from '../HelpButton';
79 79
 import OverflowMenuButton from './OverflowMenuButton';
80 80
 import OverflowMenuProfileItem from './OverflowMenuProfileItem';
81
+import MuteEveryoneButton from './MuteEveryoneButton';
81 82
 import ToolbarButton from './ToolbarButton';
82 83
 import VideoMuteButton from '../VideoMuteButton';
83 84
 import {
@@ -1000,6 +1001,10 @@ class Toolbox extends Component<Props, State> {
1000 1001
                 key = 'settings'
1001 1002
                 showLabel = { true }
1002 1003
                 visible = { this._shouldShowButton('settings') } />,
1004
+            <MuteEveryoneButton
1005
+                key = 'mute-everyone'
1006
+                showLabel = { true }
1007
+                visible = { true || this._shouldShowButton('mute-everyone') } />,
1003 1008
             this._shouldShowButton('stats')
1004 1009
                 && <OverflowMenuItem
1005 1010
                     accessibilityLabel = { t('toolbar.accessibilityLabel.speakerStats') }

正在加载...
取消
保存