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

This commit is contained in:
Andrei Oltean 2021-11-10 13:19:40 +02:00 committed by Calinteodor
parent 646fdef6bb
commit a077043f1b
27 changed files with 583 additions and 200 deletions

View File

@ -65,6 +65,11 @@
text-align: left; text-align: left;
flex: 1; flex: 1;
} }
.moderator-settings-wrapper {
padding-top: 20px;
}
.profile-edit-field { .profile-edit-field {
margin-right: 20px; margin-right: 20px;
} }

View File

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

View File

@ -98,11 +98,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
} }
dispatch(showNotification({ dispatch(showNotification({
customActionNameKey: 'notify.raiseHandAction', customActionNameKey: [ 'notify.raiseHandAction' ],
customActionHandler: () => batch(() => { customActionHandler: [ () => batch(() => {
dispatch(raiseHand(true)); dispatch(raiseHand(true));
dispatch(hideNotification(uid)); dispatch(hideNotification(uid));
}), }) ],
descriptionKey, descriptionKey,
sticky: true, sticky: true,
titleKey, titleKey,
@ -221,8 +221,8 @@ StateListenerRegistry.register(
dispatch(showNotification({ dispatch(showNotification({
titleKey: 'notify.hostAskedUnmute', titleKey: 'notify.hostAskedUnmute',
sticky: true, sticky: true,
customActionNameKey: 'notify.unmute', customActionNameKey: [ 'notify.unmute' ],
customActionHandler: () => dispatch(muteLocal(false, MEDIA_TYPE.AUDIO)) customActionHandler: [ () => dispatch(muteLocal(false, MEDIA_TYPE.AUDIO)) ]
}, NOTIFICATION_TIMEOUT_TYPE.STICKY)); }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
dispatch(playSound(ASKED_TO_UNMUTE_SOUND_ID)); dispatch(playSound(ASKED_TO_UNMUTE_SOUND_ID));
} }

View File

@ -172,6 +172,17 @@ export const SEND_TONES = 'SEND_TONES';
*/ */
export const SET_FOLLOW_ME = 'SET_FOLLOW_ME'; export const SET_FOLLOW_ME = 'SET_FOLLOW_ME';
/**
* The type of (redux) action which updates the current known status of the
* Mute Reactions Sound feature.
*
* {
* type: SET_START_REACTIONS_MUTED,
* enabled: boolean
* }
*/
export const SET_START_REACTIONS_MUTED = 'SET_START_REACTIONS_MUTED';
/** /**
* The type of (redux) action which sets the password to join or lock a specific * The type of (redux) action which sets the password to join or lock a specific
* {@code JitsiConference}. * {@code JitsiConference}.

View File

@ -53,7 +53,8 @@ import {
SET_PASSWORD_FAILED, SET_PASSWORD_FAILED,
SET_ROOM, SET_ROOM,
SET_PENDING_SUBJECT_CHANGE, SET_PENDING_SUBJECT_CHANGE,
SET_START_MUTED_POLICY SET_START_MUTED_POLICY,
SET_START_REACTIONS_MUTED
} from './actionTypes'; } from './actionTypes';
import { import {
AVATAR_URL_COMMAND, AVATAR_URL_COMMAND,
@ -669,6 +670,22 @@ export function setFollowMe(enabled: boolean) {
}; };
} }
/**
* Enables or disables the Mute reaction sounds feature.
*
* @param {boolean} muted - Whether or not reaction sounds should be muted for all participants.
* @returns {{
* type: SET_START_REACTIONS_MUTED,
* muted: boolean
* }}
*/
export function setStartReactionsMuted(muted: boolean) {
return {
type: SET_START_REACTIONS_MUTED,
muted
};
}
/** /**
* Sets the password to join or lock a specific JitsiConference. * Sets the password to join or lock a specific JitsiConference.
* *

View File

@ -20,7 +20,8 @@ import {
SET_PASSWORD, SET_PASSWORD,
SET_PENDING_SUBJECT_CHANGE, SET_PENDING_SUBJECT_CHANGE,
SET_ROOM, SET_ROOM,
SET_START_MUTED_POLICY SET_START_MUTED_POLICY,
SET_START_REACTIONS_MUTED
} from './actionTypes'; } from './actionTypes';
import { isRoomValid } from './functions'; import { isRoomValid } from './functions';
@ -77,6 +78,9 @@ ReducerRegistry.register(
case SET_FOLLOW_ME: case SET_FOLLOW_ME:
return set(state, 'followMeEnabled', action.enabled); return set(state, 'followMeEnabled', action.enabled);
case SET_START_REACTIONS_MUTED:
return set(state, 'startReactionsMuted', action.muted);
case SET_LOCATION_URL: case SET_LOCATION_URL:
return set(state, 'room', undefined); return set(state, 'room', undefined);

View File

@ -2,7 +2,11 @@
import UIEvents from '../../../../service/UI/UIEvents'; import UIEvents from '../../../../service/UI/UIEvents';
import { processExternalDeviceRequest } from '../../device-selection'; import { processExternalDeviceRequest } from '../../device-selection';
import { NOTIFICATION_TIMEOUT_TYPE, showNotification, showWarningNotification } from '../../notifications'; import {
NOTIFICATION_TIMEOUT_TYPE,
showNotification,
showWarningNotification
} from '../../notifications';
import { replaceAudioTrackById, replaceVideoTrackById, setDeviceStatusWarning } from '../../prejoin/actions'; import { replaceAudioTrackById, replaceVideoTrackById, setDeviceStatusWarning } from '../../prejoin/actions';
import { isPrejoinPageVisible } from '../../prejoin/functions'; import { isPrejoinPageVisible } from '../../prejoin/functions';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app'; import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
@ -294,8 +298,8 @@ function _checkAndNotifyForNewDevice(store, newDevices, oldDevices) {
dispatch(showNotification({ dispatch(showNotification({
description, description,
titleKey, titleKey,
customActionNameKey: 'notify.newDeviceAction', customActionNameKey: [ 'notify.newDeviceAction' ],
customActionHandler: _useDevice.bind(undefined, store, devicesArray) customActionHandler: [ _useDevice.bind(undefined, store, devicesArray) ]
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM)); }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
} }
}); });

View File

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

View File

@ -14,7 +14,8 @@
* serverURL: string, * serverURL: string,
* startAudioOnly: boolean, * startAudioOnly: boolean,
* startWithAudioMuted: boolean, * startWithAudioMuted: boolean,
* startWithVideoMuted: boolean * startWithVideoMuted: boolean,
* startWithReactionsMuted: boolean
* } * }
* } * }
*/ */

View File

@ -15,9 +15,11 @@ import { SETTINGS_UPDATED } from './actionTypes';
* localFlipX: boolean, * localFlipX: boolean,
* micDeviceId: string, * micDeviceId: string,
* serverURL: string, * serverURL: string,
* soundsReactions: boolean,
* startAudioOnly: boolean, * startAudioOnly: boolean,
* startWithAudioMuted: boolean, * startWithAudioMuted: boolean,
* startWithVideoMuted: boolean * startWithVideoMuted: boolean,
* startWithReactionsMuted: boolean
* } * }
* }} * }}
*/ */

View File

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

View File

@ -20,12 +20,12 @@ export type Props = {
/** /**
* Callback invoked when the custom button is clicked. * Callback invoked when the custom button is clicked.
*/ */
customActionHandler: Function, customActionHandler: Function[],
/** /**
* The text to display as button in the notification for the custom action. * The text to display as button in the notification for the custom action.
*/ */
customActionNameKey: string, customActionNameKey: string[],
/** /**
* The text to display in the body of the notification. If not passed * The text to display in the body of the notification. If not passed

View File

@ -128,17 +128,17 @@ class Notification extends AbstractNotification<Props> {
]; ];
default: default:
if (this.props.customActionNameKey && this.props.customActionHandler) { if (this.props.customActionNameKey?.length && this.props.customActionHandler?.length) {
return [ return this.props.customActionNameKey.map((customAction: string, customActionIndex: number) => {
{ return {
content: this.props.t(this.props.customActionNameKey), content: this.props.t(customAction),
onClick: () => { onClick: () => {
if (this.props.customActionHandler()) { if (this.props.customActionHandler[customActionIndex]()) {
this._onDismissed(); this._onDismissed();
} }
} }
} };
]; });
} }
return []; return [];

View File

@ -17,12 +17,17 @@ import {
} from '../../../av-moderation/functions'; } from '../../../av-moderation/functions';
import { ContextMenu, ContextMenuItemGroup } from '../../../base/components'; import { ContextMenu, ContextMenuItemGroup } from '../../../base/components';
import { openDialog } from '../../../base/dialog'; import { openDialog } from '../../../base/dialog';
import { IconCheck, IconVideoOff } from '../../../base/icons'; import {
IconCheck,
IconHorizontalPoints,
IconVideoOff
} from '../../../base/icons';
import { MEDIA_TYPE } from '../../../base/media'; import { MEDIA_TYPE } from '../../../base/media';
import { import {
getParticipantCount, getParticipantCount,
isEveryoneModerator isEveryoneModerator
} from '../../../base/participants'; } from '../../../base/participants';
import { openSettingsDialog, SETTINGS_TABS } from '../../../settings';
import { MuteEveryonesVideoDialog } from '../../../video-menu/components'; import { MuteEveryonesVideoDialog } from '../../../video-menu/components';
const useStyles = makeStyles(theme => { const useStyles = makeStyles(theme => {
@ -95,6 +100,8 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: Props
const muteAllVideo = useCallback( const muteAllVideo = useCallback(
() => dispatch(openDialog(MuteEveryonesVideoDialog)), [ dispatch ]); () => dispatch(openDialog(MuteEveryonesVideoDialog)), [ dispatch ]);
const openModeratorSettings = () => dispatch(openSettingsDialog(SETTINGS_TABS.MODERATOR));
const actions = [ const actions = [
{ {
accessibilityLabel: t('participantsPane.actions.audioModeration'), accessibilityLabel: t('participantsPane.actions.audioModeration'),
@ -139,6 +146,14 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: Props
</div> </div>
</ContextMenuItemGroup> </ContextMenuItemGroup>
)} )}
<ContextMenuItemGroup
actions = { [ {
accessibilityLabel: t('participantsPane.actions.moreModerationControls'),
id: 'participants-pane-open-moderation-control-settings',
icon: IconHorizontalPoints,
onClick: openModeratorSettings,
text: t('participantsPane.actions.moreModerationControls')
} ] } />
</ContextMenu> </ContextMenu>
); );
}; };

View File

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

View File

@ -4,9 +4,15 @@ import { batch } from 'react-redux';
import { createReactionSoundsDisabledEvent, sendAnalytics } from '../analytics'; import { createReactionSoundsDisabledEvent, sendAnalytics } from '../analytics';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app'; import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
import { getParticipantCount } from '../base/participants'; import { CONFERENCE_WILL_JOIN, setStartReactionsMuted } from '../base/conference';
import {
getParticipantById,
getParticipantCount,
isLocalParticipantModerator
} from '../base/participants';
import { MiddlewareRegistry } from '../base/redux'; import { MiddlewareRegistry } from '../base/redux';
import { SETTINGS_UPDATED, updateSettings } from '../base/settings'; import { SETTINGS_UPDATED } from '../base/settings/actionTypes';
import { updateSettings } from '../base/settings/actions';
import { playSound, registerSound, unregisterSound } from '../base/sounds'; import { playSound, registerSound, unregisterSound } from '../base/sounds';
import { getDisabledSounds } from '../base/sounds/functions.any'; import { getDisabledSounds } from '../base/sounds/functions.any';
import { NOTIFICATION_TIMEOUT_TYPE, showNotification } from '../notifications'; import { NOTIFICATION_TIMEOUT_TYPE, showNotification } from '../notifications';
@ -31,7 +37,8 @@ import {
RAISE_HAND_SOUND_ID, RAISE_HAND_SOUND_ID,
REACTIONS, REACTIONS,
REACTION_SOUND, REACTION_SOUND,
SOUNDS_THRESHOLDS SOUNDS_THRESHOLDS,
MUTE_REACTIONS_COMMAND
} from './constants'; } from './constants';
import { import {
getReactionMessageFromBuffer, getReactionMessageFromBuffer,
@ -39,8 +46,11 @@ import {
getReactionsWithId, getReactionsWithId,
sendReactionsWebhook sendReactionsWebhook
} from './functions.any'; } from './functions.any';
import logger from './logger';
import { RAISE_HAND_SOUND_FILE } from './sounds'; import { RAISE_HAND_SOUND_FILE } from './sounds';
import './subscriber';
declare var APP: Object; declare var APP: Object;
@ -95,7 +105,15 @@ MiddlewareRegistry.register(store => next => action => {
break; break;
} }
case CONFERENCE_WILL_JOIN: {
const { conference } = action;
conference.addCommandListener(
MUTE_REACTIONS_COMMAND, ({ attributes }, id) => {
_onMuteReactionsCommand(attributes, id, store);
});
break;
}
case FLUSH_REACTION_BUFFER: { case FLUSH_REACTION_BUFFER: {
const state = getState(); const state = getState();
const { buffer } = state['features/reactions']; const { buffer } = state['features/reactions'];
@ -163,12 +181,26 @@ MiddlewareRegistry.register(store => next => action => {
} }
case SHOW_SOUNDS_NOTIFICATION: { case SHOW_SOUNDS_NOTIFICATION: {
const state = getState();
const isModerator = isLocalParticipantModerator(state);
const customActions = [ 'notify.reactionSounds' ];
const customFunctions = [ () => dispatch(updateSettings({
soundsReactions: false
})) ];
if (isModerator) {
customActions.push('notify.reactionSoundsForAll');
customFunctions.push(() => batch(() => {
dispatch(setStartReactionsMuted(true));
dispatch(updateSettings({ soundsReactions: false }));
}));
}
dispatch(showNotification({ dispatch(showNotification({
titleKey: 'toolbar.disableReactionSounds', titleKey: 'toolbar.disableReactionSounds',
customActionNameKey: 'notify.reactionSounds', customActionNameKey: customActions,
customActionHandler: () => dispatch(updateSettings({ customActionHandler: customFunctions
soundsReactions: false
}))
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM)); }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
break; break;
} }
@ -176,3 +208,51 @@ MiddlewareRegistry.register(store => next => action => {
return next(action); return next(action);
}); });
/**
* Notifies this instance about a "Mute Reaction Sounds" command received by the Jitsi
* conference.
*
* @param {Object} attributes - The attributes carried by the command.
* @param {string} id - The identifier of the participant who issuing the
* command. A notable idiosyncrasy to be mindful of here is that the command
* may be issued by the local participant.
* @param {Object} store - The redux store. Used to calculate and dispatch
* updates.
* @private
* @returns {void}
*/
function _onMuteReactionsCommand(attributes = {}, id, store) {
const state = store.getState();
// We require to know who issued the command because (1) only a
// moderator is allowed to send commands and (2) a command MUST be
// issued by a defined commander.
if (typeof id === 'undefined') {
return;
}
const participantSendingCommand = getParticipantById(state, id);
// The Command(s) API will send us our own commands and we don't want
// to act upon them.
if (participantSendingCommand.local) {
return;
}
if (participantSendingCommand.role !== 'moderator') {
logger.warn('Received mute-reactions command not from moderator');
return;
}
const oldState = Boolean(state['features/base/conference'].startReactionsMuted);
const newState = attributes.startReactionsMuted === 'true';
if (oldState !== newState) {
batch(() => {
store.dispatch(setStartReactionsMuted(newState));
store.dispatch(updateSettings({ soundsReactions: !newState }));
});
}
}

View File

@ -0,0 +1,45 @@
// @flow
import { getCurrentConference } from '../base/conference';
import { isLocalParticipantModerator } from '../base/participants';
import { StateListenerRegistry } from '../base/redux';
import { MUTE_REACTIONS_COMMAND } from './constants';
/**
* Subscribes to changes to the Mute Reaction Sounds setting for the local participant to
* notify remote participants of current user interface status.
* Changing newSelectedValue param to off, when feature is turned of so we can
* notify all listeners.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/base/conference'].startReactionsMuted,
/* listener */ (newSelectedValue, store) => _sendMuteReactionsCommand(newSelectedValue || false, store));
/**
* Sends the mute-reactions command, when a local property change occurs.
*
* @param {*} newSelectedValue - The changed selected value from the selector.
* @param {Object} store - The redux store.
* @private
* @returns {void}
*/
function _sendMuteReactionsCommand(newSelectedValue, store) {
const state = store.getState();
const conference = getCurrentConference(state);
if (!conference) {
return;
}
// Only a moderator is allowed to send commands.
if (!isLocalParticipantModerator(state)) {
return;
}
conference.sendCommand(
MUTE_REACTIONS_COMMAND,
{ attributes: { startReactionsMuted: Boolean(newSelectedValue) } }
);
}

View File

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

View File

@ -1,6 +1,11 @@
// @flow // @flow
import { batch } from 'react-redux';
import { setFollowMe, setStartMutedPolicy } from '../base/conference'; import {
setFollowMe,
setStartMutedPolicy,
setStartReactionsMuted
} from '../base/conference';
import { openDialog } from '../base/dialog'; import { openDialog } from '../base/dialog';
import { i18next } from '../base/i18n'; import { i18next } from '../base/i18n';
import { updateSettings } from '../base/settings'; import { updateSettings } from '../base/settings';
@ -12,7 +17,12 @@ import {
SET_VIDEO_SETTINGS_VISIBILITY SET_VIDEO_SETTINGS_VISIBILITY
} from './actionTypes'; } from './actionTypes';
import { LogoutDialog, SettingsDialog } from './components'; import { LogoutDialog, SettingsDialog } from './components';
import { getMoreTabProps, getProfileTabProps, getSoundsTabProps } from './functions'; import {
getModeratorTabProps,
getMoreTabProps,
getProfileTabProps,
getSoundsTabProps
} from './functions';
declare var APP: Object; declare var APP: Object;
@ -74,10 +84,6 @@ export function submitMoreTab(newState: Object): Function {
return (dispatch, getState) => { return (dispatch, getState) => {
const currentState = getMoreTabProps(getState()); const currentState = getMoreTabProps(getState());
if (newState.followMeEnabled !== currentState.followMeEnabled) {
dispatch(setFollowMe(newState.followMeEnabled));
}
const showPrejoinPage = newState.showPrejoinPage; const showPrejoinPage = newState.showPrejoinPage;
if (showPrejoinPage !== currentState.showPrejoinPage) { if (showPrejoinPage !== currentState.showPrejoinPage) {
@ -91,12 +97,6 @@ export function submitMoreTab(newState: Object): Function {
})); }));
} }
if (newState.startAudioMuted !== currentState.startAudioMuted
|| newState.startVideoMuted !== currentState.startVideoMuted) {
dispatch(setStartMutedPolicy(
newState.startAudioMuted, newState.startVideoMuted));
}
if (newState.currentLanguage !== currentState.currentLanguage) { if (newState.currentLanguage !== currentState.currentLanguage) {
i18next.changeLanguage(newState.currentLanguage); i18next.changeLanguage(newState.currentLanguage);
} }
@ -109,6 +109,35 @@ export function submitMoreTab(newState: Object): Function {
}; };
} }
/**
* Submits the settings from the "Moderator" tab of the settings dialog.
*
* @param {Object} newState - The new settings.
* @returns {Function}
*/
export function submitModeratorTab(newState: Object): Function {
return (dispatch, getState) => {
const currentState = getModeratorTabProps(getState());
if (newState.followMeEnabled !== currentState.followMeEnabled) {
dispatch(setFollowMe(newState.followMeEnabled));
}
if (newState.startReactionsMuted !== currentState.startReactionsMuted) {
batch(() => {
dispatch(setStartReactionsMuted(newState.startReactionsMuted));
dispatch(updateSettings({ soundsReactions: !newState.startReactionsMuted }));
});
}
if (newState.startAudioMuted !== currentState.startAudioMuted
|| newState.startVideoMuted !== currentState.startVideoMuted) {
dispatch(setStartMutedPolicy(
newState.startAudioMuted, newState.startVideoMuted));
}
};
}
/** /**
* Submits the settings from the "Profile" tab of the settings dialog. * Submits the settings from the "Profile" tab of the settings dialog.
* *
@ -138,6 +167,7 @@ export function submitProfileTab(newState: Object): Function {
export function submitSoundsTab(newState: Object): Function { export function submitSoundsTab(newState: Object): Function {
return (dispatch, getState) => { return (dispatch, getState) => {
const currentState = getSoundsTabProps(getState()); const currentState = getSoundsTabProps(getState());
const shouldNotUpdateReactionSounds = getModeratorTabProps(getState()).startReactionsMuted;
const shouldUpdate = (newState.soundsIncomingMessage !== currentState.soundsIncomingMessage) const shouldUpdate = (newState.soundsIncomingMessage !== currentState.soundsIncomingMessage)
|| (newState.soundsParticipantJoined !== currentState.soundsParticipantJoined) || (newState.soundsParticipantJoined !== currentState.soundsParticipantJoined)
|| (newState.soundsParticipantLeft !== currentState.soundsParticipantLeft) || (newState.soundsParticipantLeft !== currentState.soundsParticipantLeft)
@ -145,13 +175,18 @@ export function submitSoundsTab(newState: Object): Function {
|| (newState.soundsReactions !== currentState.soundsReactions); || (newState.soundsReactions !== currentState.soundsReactions);
if (shouldUpdate) { if (shouldUpdate) {
dispatch(updateSettings({ const settingsToUpdate = {
soundsIncomingMessage: newState.soundsIncomingMessage, soundsIncomingMessage: newState.soundsIncomingMessage,
soundsParticipantJoined: newState.soundsParticipantJoined, soundsParticipantJoined: newState.soundsParticipantJoined,
soundsParticipantLeft: newState.soundsParticipantLeft, soundsParticipantLeft: newState.soundsParticipantLeft,
soundsTalkWhileMuted: newState.soundsTalkWhileMuted, soundsTalkWhileMuted: newState.soundsTalkWhileMuted,
soundsReactions: newState.soundsReactions soundsReactions: newState.soundsReactions
})); };
if (shouldNotUpdateReactionSounds) {
delete settingsToUpdate.soundsReactions;
}
dispatch(updateSettings(settingsToUpdate));
} }
}; };
} }

View File

@ -67,6 +67,8 @@ export class AbstractSettingsView<P: Props, S: *> extends Component<P, S> {
= this._onStartAudioMutedChange.bind(this); = this._onStartAudioMutedChange.bind(this);
this._onStartVideoMutedChange this._onStartVideoMutedChange
= this._onStartVideoMutedChange.bind(this); = this._onStartVideoMutedChange.bind(this);
this._onStartReactionsMutedChange
= this._onStartReactionsMutedChange.bind(this);
} }
_onChangeDisplayName: (string) => void; _onChangeDisplayName: (string) => void;
@ -146,6 +148,22 @@ export class AbstractSettingsView<P: Props, S: *> extends Component<P, S> {
}); });
} }
_onStartReactionsMutedChange: (boolean) => void;
/**
* Handles the start reactions muted change event.
*
* @param {boolean} newValue - The new value for the start reactions muted
* option.
* @protected
* @returns {void}
*/
_onStartReactionsMutedChange(newValue) {
this._updateSettings({
startWithReactionsMuted: newValue
});
}
_updateSettings: (Object) => void; _updateSettings: (Object) => void;
/** /**

View File

@ -0,0 +1,185 @@
// @flow
import { Checkbox } from '@atlaskit/checkbox';
import React from 'react';
import { AbstractDialogTab } from '../../../base/dialog';
import type { Props as AbstractDialogTabProps } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
/**
* The type of the React {@code Component} props of {@link MoreTab}.
*/
export type Props = {
...$Exact<AbstractDialogTabProps>,
/**
* Whether or not follow me is currently active (enabled by some other participant).
*/
followMeActive: boolean,
/**
* Whether or not the user has selected the Follow Me feature to be enabled.
*/
followMeEnabled: boolean,
/**
* Whether or not the user has selected the Start Audio Muted feature to be
* enabled.
*/
startAudioMuted: boolean,
/**
* Whether or not the user has selected the Start Video Muted feature to be
* enabled.
*/
startVideoMuted: boolean,
/**
* Whether or not the user has selected the Start Reactions Muted feature to be
* enabled.
*/
startReactionsMuted: boolean,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* React {@code Component} for modifying language and moderator settings.
*
* @augments Component
*/
class ModeratorTab extends AbstractDialogTab<Props> {
/**
* Initializes a new {@code MoreTab} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onStartAudioMutedChanged = this._onStartAudioMutedChanged.bind(this);
this._onStartVideoMutedChanged = this._onStartVideoMutedChanged.bind(this);
this._onStartReactionsMutedChanged = this._onStartReactionsMutedChanged.bind(this);
this._onFollowMeEnabledChanged = this._onFollowMeEnabledChanged.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return <div className = 'moderator-tab box'>{ this._renderModeratorSettings() }</div>;
}
_onStartAudioMutedChanged: (Object) => void;
/**
* Callback invoked to select if conferences should start
* with audio muted.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onStartAudioMutedChanged({ target: { checked } }) {
super._onChange({ startAudioMuted: checked });
}
_onStartVideoMutedChanged: (Object) => void;
/**
* Callback invoked to select if conferences should start
* with video disabled.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onStartVideoMutedChanged({ target: { checked } }) {
super._onChange({ startVideoMuted: checked });
}
_onStartReactionsMutedChanged: (Object) => void;
/**
* Callback invoked to select if conferences should start
* with reactions muted.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onStartReactionsMutedChanged({ target: { checked } }) {
super._onChange({ startReactionsMuted: checked });
}
_onFollowMeEnabledChanged: (Object) => void;
/**
* Callback invoked to select if follow-me mode
* should be activated.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onFollowMeEnabledChanged({ target: { checked } }) {
super._onChange({ followMeEnabled: checked });
}
/**
* Returns the React Element for modifying conference-wide settings.
*
* @private
* @returns {ReactElement}
*/
_renderModeratorSettings() {
const {
followMeActive,
followMeEnabled,
startAudioMuted,
startVideoMuted,
startReactionsMuted,
t
} = this.props;
return (
<div
className = 'settings-sub-pane-element'
key = 'moderator'>
<div className = 'moderator-settings-wrapper'>
<Checkbox
isChecked = { startAudioMuted }
label = { t('settings.startAudioMuted') }
name = 'start-audio-muted'
onChange = { this._onStartAudioMutedChanged } />
<Checkbox
isChecked = { startVideoMuted }
label = { t('settings.startVideoMuted') }
name = 'start-video-muted'
onChange = { this._onStartVideoMutedChanged } />
<Checkbox
isChecked = { followMeEnabled && !followMeActive }
isDisabled = { followMeActive }
label = { t('settings.followMe') }
name = 'follow-me'
onChange = { this._onFollowMeEnabledChanged } />
<Checkbox
isChecked = { startReactionsMuted }
label = { t('settings.startReactionsMuted') }
name = 'start-reactions-muted'
onChange = { this._onStartReactionsMutedChanged } />
</div>
</div>
);
}
}
export default translate(ModeratorTab);

View File

@ -40,11 +40,6 @@ export type Props = {
*/ */
followMeActive: boolean, followMeActive: boolean,
/**
* Whether or not the user has selected the Follow Me feature to be enabled.
*/
followMeEnabled: boolean,
/** /**
* All available languages to display in the language select dropdown. * All available languages to display in the language select dropdown.
*/ */
@ -70,18 +65,6 @@ export type Props = {
*/ */
showPrejoinPage: boolean, showPrejoinPage: boolean,
/**
* Whether or not the user has selected the Start Audio Muted feature to be
* enabled.
*/
startAudioMuted: boolean,
/**
* Whether or not the user has selected the Start Video Muted feature to be
* enabled.
*/
startVideoMuted: boolean,
/** /**
* Invoked to obtain translated strings. * Invoked to obtain translated strings.
*/ */
@ -129,9 +112,6 @@ class MoreTab extends AbstractDialogTab<Props, State> {
this._onFramerateItemSelect = this._onFramerateItemSelect.bind(this); this._onFramerateItemSelect = this._onFramerateItemSelect.bind(this);
this._onLanguageDropdownOpenChange = this._onLanguageDropdownOpenChange.bind(this); this._onLanguageDropdownOpenChange = this._onLanguageDropdownOpenChange.bind(this);
this._onLanguageItemSelect = this._onLanguageItemSelect.bind(this); this._onLanguageItemSelect = this._onLanguageItemSelect.bind(this);
this._onStartAudioMutedChanged = this._onStartAudioMutedChanged.bind(this);
this._onStartVideoMutedChanged = this._onStartVideoMutedChanged.bind(this);
this._onFollowMeEnabledChanged = this._onFollowMeEnabledChanged.bind(this);
this._onShowPrejoinPageChanged = this._onShowPrejoinPageChanged.bind(this); this._onShowPrejoinPageChanged = this._onShowPrejoinPageChanged.bind(this);
this._onKeyboardShortcutEnableChanged = this._onKeyboardShortcutEnableChanged.bind(this); this._onKeyboardShortcutEnableChanged = this._onKeyboardShortcutEnableChanged.bind(this);
} }
@ -148,7 +128,13 @@ class MoreTab extends AbstractDialogTab<Props, State> {
content.push(this._renderSettingsLeft()); content.push(this._renderSettingsLeft());
content.push(this._renderSettingsRight()); content.push(this._renderSettingsRight());
return <div className = 'more-tab box'>{ content }</div>; return (
<div
className = 'more-tab box'
key = 'more'>
{ content }
</div>
);
} }
_onFramerateDropdownOpenChange: (Object) => void; _onFramerateDropdownOpenChange: (Object) => void;
@ -207,48 +193,6 @@ class MoreTab extends AbstractDialogTab<Props, State> {
super._onChange({ currentLanguage: language }); super._onChange({ currentLanguage: language });
} }
_onStartAudioMutedChanged: (Object) => void;
/**
* Callback invoked to select if conferences should start
* with audio muted.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onStartAudioMutedChanged({ target: { checked } }) {
super._onChange({ startAudioMuted: checked });
}
_onStartVideoMutedChanged: (Object) => void;
/**
* Callback invoked to select if conferences should start
* with video disabled.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onStartVideoMutedChanged({ target: { checked } }) {
super._onChange({ startVideoMuted: checked });
}
_onFollowMeEnabledChanged: (Object) => void;
/**
* Callback invoked to select if follow-me mode
* should be activated.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onFollowMeEnabledChanged({ target: { checked } }) {
super._onChange({ followMeEnabled: checked });
}
_onShowPrejoinPageChanged: (Object) => void; _onShowPrejoinPageChanged: (Object) => void;
/** /**
@ -410,48 +354,6 @@ class MoreTab extends AbstractDialogTab<Props, State> {
); );
} }
/**
* Returns the React Element for modifying conference-wide settings.
*
* @private
* @returns {ReactElement}
*/
_renderModeratorSettings() {
const {
followMeActive,
followMeEnabled,
startAudioMuted,
startVideoMuted,
t
} = this.props;
return (
<div
className = 'settings-sub-pane-element'
key = 'moderator'>
<h2 className = 'mock-atlaskit-label'>
{ t('settings.moderator') }
</h2>
<Checkbox
isChecked = { startAudioMuted }
label = { t('settings.startAudioMuted') }
name = 'start-audio-muted'
onChange = { this._onStartAudioMutedChanged } />
<Checkbox
isChecked = { startVideoMuted }
label = { t('settings.startVideoMuted') }
name = 'start-video-muted'
onChange = { this._onStartVideoMutedChanged } />
<Checkbox
isChecked = { followMeEnabled && !followMeActive }
isDisabled = { followMeActive }
label = { t('settings.followMe') }
name = 'follow-me'
onChange = { this._onFollowMeEnabledChanged } />
</div>
);
}
/** /**
* Returns the React Element for modifying prejoin screen settings. * Returns the React Element for modifying prejoin screen settings.
* *
@ -488,7 +390,8 @@ class MoreTab extends AbstractDialogTab<Props, State> {
return ( return (
<div <div
className = 'settings-sub-pane right'> className = 'settings-sub-pane right'
key = 'settings-sub-pane-right'>
{ showLanguageSettings && this._renderLanguageSelect() } { showLanguageSettings && this._renderLanguageSelect() }
{ this._renderFramerateSelect() } { this._renderFramerateSelect() }
</div> </div>
@ -501,14 +404,14 @@ class MoreTab extends AbstractDialogTab<Props, State> {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
_renderSettingsLeft() { _renderSettingsLeft() {
const { showPrejoinSettings, showModeratorSettings } = this.props; const { showPrejoinSettings } = this.props;
return ( return (
<div <div
className = 'settings-sub-pane left'> className = 'settings-sub-pane left'
key = 'settings-sub-pane-left'>
{ showPrejoinSettings && this._renderPrejoinScreenSettings() } { showPrejoinSettings && this._renderPrejoinScreenSettings() }
{ this._renderKeyboardShortcutCheckbox() } { this._renderKeyboardShortcutCheckbox() }
{ showModeratorSettings && this._renderModeratorSettings() }
</div> </div>
); );
} }

View File

@ -11,11 +11,22 @@ import {
getDeviceSelectionDialogProps, getDeviceSelectionDialogProps,
submitDeviceSelectionTab submitDeviceSelectionTab
} from '../../../device-selection'; } from '../../../device-selection';
import { submitMoreTab, submitProfileTab, submitSoundsTab } from '../../actions'; import {
submitModeratorTab,
submitMoreTab,
submitProfileTab,
submitSoundsTab
} from '../../actions';
import { SETTINGS_TABS } from '../../constants'; import { SETTINGS_TABS } from '../../constants';
import { getMoreTabProps, getProfileTabProps, getSoundsTabProps } from '../../functions'; import {
getModeratorTabProps,
getMoreTabProps,
getProfileTabProps,
getSoundsTabProps
} from '../../functions';
import CalendarTab from './CalendarTab'; import CalendarTab from './CalendarTab';
import ModeratorTab from './ModeratorTab';
import MoreTab from './MoreTab'; import MoreTab from './MoreTab';
import ProfileTab from './ProfileTab'; import ProfileTab from './ProfileTab';
import SoundsTab from './SoundsTab'; import SoundsTab from './SoundsTab';
@ -131,7 +142,9 @@ function _mapStateToProps(state) {
// The settings sections to display. // The settings sections to display.
const showDeviceSettings = configuredTabs.includes('devices'); const showDeviceSettings = configuredTabs.includes('devices');
const moreTabProps = getMoreTabProps(state); const moreTabProps = getMoreTabProps(state);
const { showModeratorSettings, showLanguageSettings, showPrejoinSettings } = moreTabProps; const moderatorTabProps = getModeratorTabProps(state);
const { showModeratorSettings } = moderatorTabProps;
const { showLanguageSettings, showPrejoinSettings } = moreTabProps;
const showProfileSettings const showProfileSettings
= configuredTabs.includes('profile') && !state['features/base/config'].disableProfile; = configuredTabs.includes('profile') && !state['features/base/config'].disableProfile;
const showCalendarSettings const showCalendarSettings
@ -176,6 +189,28 @@ function _mapStateToProps(state) {
}); });
} }
if (showModeratorSettings) {
tabs.push({
name: SETTINGS_TABS.MODERATOR,
component: ModeratorTab,
label: 'settings.moderator',
props: moderatorTabProps,
propsUpdateFunction: (tabState, newProps) => {
// Updates tab props, keeping users selection
return {
...newProps,
followMeEnabled: tabState.followMeEnabled,
startAudioMuted: tabState.startAudioMuted,
startVideoMuted: tabState.startVideoMuted,
startReactionsMuted: tabState.startReactionsMuted
};
},
styles: 'settings-pane moderator-pane',
submit: submitModeratorTab
});
}
if (showCalendarSettings) { if (showCalendarSettings) {
tabs.push({ tabs.push({
name: SETTINGS_TABS.CALENDAR, name: SETTINGS_TABS.CALENDAR,
@ -196,7 +231,7 @@ function _mapStateToProps(state) {
}); });
} }
if (showModeratorSettings || showLanguageSettings || showPrejoinSettings) { if (showLanguageSettings || showPrejoinSettings) {
tabs.push({ tabs.push({
name: SETTINGS_TABS.MORE, name: SETTINGS_TABS.MORE,
component: MoreTab, component: MoreTab,
@ -209,10 +244,7 @@ function _mapStateToProps(state) {
...newProps, ...newProps,
currentFramerate: tabState.currentFramerate, currentFramerate: tabState.currentFramerate,
currentLanguage: tabState.currentLanguage, currentLanguage: tabState.currentLanguage,
followMeEnabled: tabState.followMeEnabled, showPrejoinPage: tabState.showPrejoinPage
showPrejoinPage: tabState.showPrejoinPage,
startAudioMuted: tabState.startAudioMuted,
startVideoMuted: tabState.startVideoMuted
}; };
}, },
styles: 'settings-pane more-pane', styles: 'settings-pane more-pane',

View File

@ -45,6 +45,11 @@ export type Props = {
*/ */
soundsReactions: Boolean, soundsReactions: Boolean,
/**
* Whether or not moderator muted the sounds.
*/
moderatorMutedSoundsReactions: Boolean,
/** /**
* Invoked to obtain translated strings. * Invoked to obtain translated strings.
*/ */
@ -97,6 +102,7 @@ class SoundsTab extends AbstractDialogTab<Props> {
soundsTalkWhileMuted, soundsTalkWhileMuted,
soundsReactions, soundsReactions,
enableReactions, enableReactions,
moderatorMutedSoundsReactions,
t t
} = this.props; } = this.props;
@ -109,6 +115,7 @@ class SoundsTab extends AbstractDialogTab<Props> {
</h2> </h2>
{enableReactions && <Checkbox {enableReactions && <Checkbox
isChecked = { soundsReactions } isChecked = { soundsReactions }
isDisabled = { moderatorMutedSoundsReactions }
label = { t('settings.reactions') } label = { t('settings.reactions') }
name = 'soundsReactions' name = 'soundsReactions'
onChange = { this._onChange } /> onChange = { this._onChange } />

View File

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

View File

@ -78,17 +78,6 @@ export function normalizeUserInputURL(url: string) {
/* eslint-enable no-param-reassign */ /* eslint-enable no-param-reassign */
} }
/**
* Used for web. Returns whether or not only Device Selection is configured to
* display as a setting.
*
* @returns {boolean}
*/
export function shouldShowOnlyDeviceSelection() {
return interfaceConfig.SETTINGS_SECTIONS.length === 1
&& isSettingEnabled('devices');
}
/** /**
* Returns the properties for the "More" tab from settings dialog from Redux * Returns the properties for the "More" tab from settings dialog from Redux
* state. * state.
@ -101,32 +90,50 @@ export function getMoreTabProps(stateful: Object | Function) {
const state = toState(stateful); const state = toState(stateful);
const framerate = state['features/screen-share'].captureFrameRate ?? SS_DEFAULT_FRAME_RATE; const framerate = state['features/screen-share'].captureFrameRate ?? SS_DEFAULT_FRAME_RATE;
const language = i18next.language || DEFAULT_LANGUAGE; const language = i18next.language || DEFAULT_LANGUAGE;
const {
conference,
followMeEnabled,
startAudioMutedPolicy,
startVideoMutedPolicy
} = state['features/base/conference'];
const followMeActive = isFollowMeActive(state);
const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || []; const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
// The settings sections to display.
const showModeratorSettings = Boolean(
conference
&& configuredTabs.includes('moderator')
&& isLocalParticipantModerator(state));
return { return {
currentFramerate: framerate, currentFramerate: framerate,
currentLanguage: language, currentLanguage: language,
desktopShareFramerates: SS_SUPPORTED_FRAMERATES, desktopShareFramerates: SS_SUPPORTED_FRAMERATES,
followMeActive: Boolean(conference && followMeActive),
followMeEnabled: Boolean(conference && followMeEnabled),
languages: LANGUAGES, languages: LANGUAGES,
showLanguageSettings: configuredTabs.includes('language'), showLanguageSettings: configuredTabs.includes('language'),
showModeratorSettings,
showPrejoinSettings: state['features/base/config'].prejoinPageEnabled,
showPrejoinPage: !state['features/base/settings'].userSelectedSkipPrejoin, showPrejoinPage: !state['features/base/settings'].userSelectedSkipPrejoin,
showPrejoinSettings: state['features/base/config'].prejoinPageEnabled
};
}
/**
* Returns the properties for the "More" tab from settings dialog from Redux
* state.
*
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state.
* @returns {Object} - The properties for the "More" tab from settings dialog.
*/
export function getModeratorTabProps(stateful: Object | Function) {
const state = toState(stateful);
const {
conference,
followMeEnabled,
startAudioMutedPolicy,
startVideoMutedPolicy,
startReactionsMuted
} = state['features/base/conference'];
const followMeActive = isFollowMeActive(state);
const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
const showModeratorSettings = Boolean(
conference
&& configuredTabs.includes('moderator')
&& isLocalParticipantModerator(state));
// The settings sections to display.
return {
showModeratorSettings,
followMeActive: Boolean(conference && followMeActive),
followMeEnabled: Boolean(conference && followMeEnabled),
startReactionsMuted: Boolean(conference && startReactionsMuted),
startAudioMuted: Boolean(conference && startAudioMutedPolicy), startAudioMuted: Boolean(conference && startAudioMutedPolicy),
startVideoMuted: Boolean(conference && startVideoMutedPolicy) startVideoMuted: Boolean(conference && startVideoMutedPolicy)
}; };
@ -178,6 +185,7 @@ export function getSoundsTabProps(stateful: Object | Function) {
soundsReactions soundsReactions
} = state['features/base/settings']; } = state['features/base/settings'];
const enableReactions = isReactionsEnabled(state); const enableReactions = isReactionsEnabled(state);
const moderatorMutedSoundsReactions = state['features/base/conference'].startReactionsMuted ?? false;
return { return {
soundsIncomingMessage, soundsIncomingMessage,
@ -185,7 +193,8 @@ export function getSoundsTabProps(stateful: Object | Function) {
soundsParticipantLeft, soundsParticipantLeft,
soundsTalkWhileMuted, soundsTalkWhileMuted,
soundsReactions, soundsReactions,
enableReactions enableReactions,
moderatorMutedSoundsReactions
}; };
} }

View File

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