Browse Source

feat(mute): mute everyone / everyone else

master
Gabriel Imre 5 years ago
parent
commit
24a1a60f04

+ 1
- 1
interface_config.js View File

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

+ 9
- 0
lang/main.json View File

209
         "micNotSendingDataTitle": "Your mic is muted by your system settings",
209
         "micNotSendingDataTitle": "Your mic is muted by your system settings",
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.",
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
         "micUnknownError": "Cannot use microphone for an unknown reason.",
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
         "muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
218
         "muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
213
         "muteParticipantButton": "Mute",
219
         "muteParticipantButton": "Mute",
214
         "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.",
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
             "moreActionsMenu": "More actions menu",
598
             "moreActionsMenu": "More actions menu",
593
             "moreOptions": "Show more options",
599
             "moreOptions": "Show more options",
594
             "mute": "Toggle mute audio",
600
             "mute": "Toggle mute audio",
601
+            "muteEveryone": "Mute everyone",
595
             "pip": "Toggle Picture-in-Picture mode",
602
             "pip": "Toggle Picture-in-Picture mode",
596
             "privateMessage": "Send private message",
603
             "privateMessage": "Send private message",
597
             "profile": "Edit your profile",
604
             "profile": "Edit your profile",
635
         "moreActions": "More actions",
642
         "moreActions": "More actions",
636
         "moreOptions": "More options",
643
         "moreOptions": "More options",
637
         "mute": "Mute / Unmute",
644
         "mute": "Mute / Unmute",
645
+        "muteEveryone": "Mute everyone",
638
         "noAudioSignalTitle": "There is no input coming from your mic!",
646
         "noAudioSignalTitle": "There is no input coming from your mic!",
639
         "noAudioSignalDesc": "If you did not purposely mute it from system settings or hardware, consider switching the device.",
647
         "noAudioSignalDesc": "If you did not purposely mute it from system settings or hardware, consider switching the device.",
640
         "noAudioSignalDescSuggestion": "If you did not purposely mute it from system settings or hardware, consider switching to the suggested device.",
648
         "noAudioSignalDescSuggestion": "If you did not purposely mute it from system settings or hardware, consider switching to the suggested device.",
722
     },
730
     },
723
     "videothumbnail": {
731
     "videothumbnail": {
724
         "domute": "Mute",
732
         "domute": "Mute",
733
+        "domuteOthers": "Mute everyone else",
725
         "flip": "Flip",
734
         "flip": "Flip",
726
         "kick": "Kick out",
735
         "kick": "Kick out",
727
         "moderator": "Moderator",
736
         "moderator": "Moderator",

+ 68
- 0
react/features/remote-video-menu/actions.js View File

1
 // @flow
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
 import { hideDialog } from '../base/dialog';
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
 import { RemoteVideoMenu } from './components';
18
 import { RemoteVideoMenu } from './components';
6
 
19
 
20
+declare var APP: Object;
21
+
7
 /**
22
 /**
8
  * Hides the remote video menu.
23
  * Hides the remote video menu.
9
  *
24
  *
12
 export function hideRemoteVideoMenu() {
27
 export function hideRemoteVideoMenu() {
13
     return hideDialog(RemoteVideoMenu);
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 View File

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

+ 123
- 0
react/features/remote-video-menu/components/web/MuteEveryoneDialog.js View File

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 View File

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 View File

9
 
9
 
10
 import {
10
 import {
11
     MuteButton,
11
     MuteButton,
12
+    MuteEveryoneElseButton,
12
     KickButton,
13
     KickButton,
13
     PrivateMessageMenuButton,
14
     PrivateMessageMenuButton,
14
     RemoteControlButton,
15
     RemoteControlButton,
174
                     key = 'mute'
175
                     key = 'mute'
175
                     participantID = { participantID } />
176
                     participantID = { participantID } />
176
             );
177
             );
178
+            buttons.push(
179
+                <MuteEveryoneElseButton
180
+                    key = 'mute-others'
181
+                    participantID = { participantID } />
182
+            );
177
             buttons.push(
183
             buttons.push(
178
                 <KickButton
184
                 <KickButton
179
                     key = 'kick'
185
                     key = 'kick'

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

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

+ 3
- 10
react/features/toolbox/components/AudioMuteButton.js View File

4
     ACTION_SHORTCUT_TRIGGERED,
4
     ACTION_SHORTCUT_TRIGGERED,
5
     AUDIO_MUTE,
5
     AUDIO_MUTE,
6
     createShortcutEvent,
6
     createShortcutEvent,
7
-    createToolbarEvent,
8
     sendAnalytics
7
     sendAnalytics
9
 } from '../../analytics';
8
 } from '../../analytics';
10
 import { translate } from '../../base/i18n';
9
 import { translate } from '../../base/i18n';
11
-import { MEDIA_TYPE, setAudioMuted } from '../../base/media';
10
+import { MEDIA_TYPE } from '../../base/media';
12
 import { connect } from '../../base/redux';
11
 import { connect } from '../../base/redux';
13
 import { AbstractAudioMuteButton } from '../../base/toolbox';
12
 import { AbstractAudioMuteButton } from '../../base/toolbox';
14
 import type { AbstractButtonProps } from '../../base/toolbox';
13
 import type { AbstractButtonProps } from '../../base/toolbox';
15
 import { isLocalTrackMuted } from '../../base/tracks';
14
 import { isLocalTrackMuted } from '../../base/tracks';
16
-import UIEvents from '../../../../service/UI/UIEvents';
15
+import { muteLocal } from '../../remote-video-menu/actions';
17
 
16
 
18
 declare var APP: Object;
17
 declare var APP: Object;
19
 
18
 
125
      * @returns {void}
124
      * @returns {void}
126
      */
125
      */
127
     _setAudioMuted(audioMuted: boolean) {
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 View File

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 View File

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

Loading…
Cancel
Save