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;
flex: 1;
}
.moderator-settings-wrapper {
padding-top: 20px;
}
.profile-edit-field {
margin-right: 20px;
}

View File

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

View File

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

View File

@ -53,7 +53,8 @@ import {
SET_PASSWORD_FAILED,
SET_ROOM,
SET_PENDING_SUBJECT_CHANGE,
SET_START_MUTED_POLICY
SET_START_MUTED_POLICY,
SET_START_REACTIONS_MUTED
} from './actionTypes';
import {
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.
*

View File

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

View File

@ -2,7 +2,11 @@
import UIEvents from '../../../../service/UI/UIEvents';
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 { isPrejoinPageVisible } from '../../prejoin/functions';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
@ -294,8 +298,8 @@ function _checkAndNotifyForNewDevice(store, newDevices, oldDevices) {
dispatch(showNotification({
description,
titleKey,
customActionNameKey: 'notify.newDeviceAction',
customActionHandler: _useDevice.bind(undefined, store, devicesArray)
customActionNameKey: [ 'notify.newDeviceAction' ],
customActionHandler: [ _useDevice.bind(undefined, store, devicesArray) ]
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
}
});

View File

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

View File

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

View File

@ -15,9 +15,11 @@ import { SETTINGS_UPDATED } from './actionTypes';
* localFlipX: boolean,
* micDeviceId: string,
* serverURL: string,
* soundsReactions: boolean,
* startAudioOnly: 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
// the description.
// TODO Add support for arguments to showNotification title and customAction strings.
customActionNameKey = `Switch to ${formatDeviceLabel(activeDevice.deviceLabel)}`;
customActionHandler = () => {
customActionNameKey = [ `Switch to ${formatDeviceLabel(activeDevice.deviceLabel)}` ];
customActionHandler = [ () => {
// Select device callback
dispatch(
updateSettings({
@ -105,7 +105,7 @@ async function _handleNoAudioSignalNotification({ dispatch, getState }, action)
);
dispatch(setAudioInputDevice(activeDevice.deviceId));
};
} ];
}
const notification = await dispatch(showNotification({

View File

@ -20,12 +20,12 @@ export type Props = {
/**
* 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.
*/
customActionNameKey: string,
customActionNameKey: string[],
/**
* 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:
if (this.props.customActionNameKey && this.props.customActionHandler) {
return [
{
content: this.props.t(this.props.customActionNameKey),
if (this.props.customActionNameKey?.length && this.props.customActionHandler?.length) {
return this.props.customActionNameKey.map((customAction: string, customActionIndex: number) => {
return {
content: this.props.t(customAction),
onClick: () => {
if (this.props.customActionHandler()) {
if (this.props.customActionHandler[customActionIndex]()) {
this._onDismissed();
}
}
}
];
};
});
}
return [];

View File

@ -17,12 +17,17 @@ import {
} from '../../../av-moderation/functions';
import { ContextMenu, ContextMenuItemGroup } from '../../../base/components';
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 {
getParticipantCount,
isEveryoneModerator
} from '../../../base/participants';
import { openSettingsDialog, SETTINGS_TABS } from '../../../settings';
import { MuteEveryonesVideoDialog } from '../../../video-menu/components';
const useStyles = makeStyles(theme => {
@ -95,6 +100,8 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: Props
const muteAllVideo = useCallback(
() => dispatch(openDialog(MuteEveryonesVideoDialog)), [ dispatch ]);
const openModeratorSettings = () => dispatch(openSettingsDialog(SETTINGS_TABS.MODERATOR));
const actions = [
{
accessibilityLabel: t('participantsPane.actions.audioModeration'),
@ -139,6 +146,14 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: Props
</div>
</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>
);
};

View File

@ -14,6 +14,13 @@ import {
*/
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.
*/

View File

@ -4,9 +4,15 @@ import { batch } from 'react-redux';
import { createReactionSoundsDisabledEvent, sendAnalytics } from '../analytics';
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 { 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 { getDisabledSounds } from '../base/sounds/functions.any';
import { NOTIFICATION_TIMEOUT_TYPE, showNotification } from '../notifications';
@ -31,7 +37,8 @@ import {
RAISE_HAND_SOUND_ID,
REACTIONS,
REACTION_SOUND,
SOUNDS_THRESHOLDS
SOUNDS_THRESHOLDS,
MUTE_REACTIONS_COMMAND
} from './constants';
import {
getReactionMessageFromBuffer,
@ -39,8 +46,11 @@ import {
getReactionsWithId,
sendReactionsWebhook
} from './functions.any';
import logger from './logger';
import { RAISE_HAND_SOUND_FILE } from './sounds';
import './subscriber';
declare var APP: Object;
@ -95,7 +105,15 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case CONFERENCE_WILL_JOIN: {
const { conference } = action;
conference.addCommandListener(
MUTE_REACTIONS_COMMAND, ({ attributes }, id) => {
_onMuteReactionsCommand(attributes, id, store);
});
break;
}
case FLUSH_REACTION_BUFFER: {
const state = getState();
const { buffer } = state['features/reactions'];
@ -163,12 +181,26 @@ MiddlewareRegistry.register(store => next => action => {
}
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({
titleKey: 'toolbar.disableReactionSounds',
customActionNameKey: 'notify.reactionSounds',
customActionHandler: () => dispatch(updateSettings({
soundsReactions: false
}))
customActionNameKey: customActions,
customActionHandler: customFunctions
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
break;
}
@ -176,3 +208,51 @@ MiddlewareRegistry.register(store => 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 participantName = getParticipantDisplayName(state, initiatorId);
let dialogProps = {
customActionNameKey: undefined,
descriptionKey: participantName ? 'liveStreaming.onBy' : 'liveStreaming.on',
descriptionArguments: { name: participantName },
isDismissAllowed: true,
@ -206,8 +205,8 @@ export function showStartedRecordingNotification(
}
// add the option to copy recording link
dialogProps.customActionNameKey = 'recording.copyLink';
dialogProps.customActionHandler = () => copyText(link);
dialogProps.customActionNameKey = [ 'recording.copyLink' ];
dialogProps.customActionHandler = [ () => copyText(link) ];
dialogProps.titleKey = 'recording.on';
dialogProps.descriptionKey = 'recording.linkGenerated';
dialogProps.isDismissAllowed = false;

View File

@ -1,6 +1,11 @@
// @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 { i18next } from '../base/i18n';
import { updateSettings } from '../base/settings';
@ -12,7 +17,12 @@ import {
SET_VIDEO_SETTINGS_VISIBILITY
} from './actionTypes';
import { LogoutDialog, SettingsDialog } from './components';
import { getMoreTabProps, getProfileTabProps, getSoundsTabProps } from './functions';
import {
getModeratorTabProps,
getMoreTabProps,
getProfileTabProps,
getSoundsTabProps
} from './functions';
declare var APP: Object;
@ -74,10 +84,6 @@ export function submitMoreTab(newState: Object): Function {
return (dispatch, getState) => {
const currentState = getMoreTabProps(getState());
if (newState.followMeEnabled !== currentState.followMeEnabled) {
dispatch(setFollowMe(newState.followMeEnabled));
}
const showPrejoinPage = newState.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) {
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.
*
@ -138,6 +167,7 @@ export function submitProfileTab(newState: Object): Function {
export function submitSoundsTab(newState: Object): Function {
return (dispatch, getState) => {
const currentState = getSoundsTabProps(getState());
const shouldNotUpdateReactionSounds = getModeratorTabProps(getState()).startReactionsMuted;
const shouldUpdate = (newState.soundsIncomingMessage !== currentState.soundsIncomingMessage)
|| (newState.soundsParticipantJoined !== currentState.soundsParticipantJoined)
|| (newState.soundsParticipantLeft !== currentState.soundsParticipantLeft)
@ -145,13 +175,18 @@ export function submitSoundsTab(newState: Object): Function {
|| (newState.soundsReactions !== currentState.soundsReactions);
if (shouldUpdate) {
dispatch(updateSettings({
const settingsToUpdate = {
soundsIncomingMessage: newState.soundsIncomingMessage,
soundsParticipantJoined: newState.soundsParticipantJoined,
soundsParticipantLeft: newState.soundsParticipantLeft,
soundsTalkWhileMuted: newState.soundsTalkWhileMuted,
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._onStartVideoMutedChange
= this._onStartVideoMutedChange.bind(this);
this._onStartReactionsMutedChange
= this._onStartReactionsMutedChange.bind(this);
}
_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;
/**

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,
/**
* 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.
*/
@ -70,18 +65,6 @@ export type Props = {
*/
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.
*/
@ -129,9 +112,6 @@ class MoreTab extends AbstractDialogTab<Props, State> {
this._onFramerateItemSelect = this._onFramerateItemSelect.bind(this);
this._onLanguageDropdownOpenChange = this._onLanguageDropdownOpenChange.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._onKeyboardShortcutEnableChanged = this._onKeyboardShortcutEnableChanged.bind(this);
}
@ -148,7 +128,13 @@ class MoreTab extends AbstractDialogTab<Props, State> {
content.push(this._renderSettingsLeft());
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;
@ -207,48 +193,6 @@ class MoreTab extends AbstractDialogTab<Props, State> {
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;
/**
@ -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.
*
@ -488,7 +390,8 @@ class MoreTab extends AbstractDialogTab<Props, State> {
return (
<div
className = 'settings-sub-pane right'>
className = 'settings-sub-pane right'
key = 'settings-sub-pane-right'>
{ showLanguageSettings && this._renderLanguageSelect() }
{ this._renderFramerateSelect() }
</div>
@ -501,14 +404,14 @@ class MoreTab extends AbstractDialogTab<Props, State> {
* @returns {ReactElement}
*/
_renderSettingsLeft() {
const { showPrejoinSettings, showModeratorSettings } = this.props;
const { showPrejoinSettings } = this.props;
return (
<div
className = 'settings-sub-pane left'>
className = 'settings-sub-pane left'
key = 'settings-sub-pane-left'>
{ showPrejoinSettings && this._renderPrejoinScreenSettings() }
{ this._renderKeyboardShortcutCheckbox() }
{ showModeratorSettings && this._renderModeratorSettings() }
</div>
);
}

View File

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

View File

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

View File

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

View File

@ -78,17 +78,6 @@ export function normalizeUserInputURL(url: string) {
/* 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
* state.
@ -101,32 +90,50 @@ export function getMoreTabProps(stateful: Object | Function) {
const state = toState(stateful);
const framerate = state['features/screen-share'].captureFrameRate ?? SS_DEFAULT_FRAME_RATE;
const language = i18next.language || DEFAULT_LANGUAGE;
const {
conference,
followMeEnabled,
startAudioMutedPolicy,
startVideoMutedPolicy
} = state['features/base/conference'];
const followMeActive = isFollowMeActive(state);
const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
// The settings sections to display.
const showModeratorSettings = Boolean(
conference
&& configuredTabs.includes('moderator')
&& isLocalParticipantModerator(state));
return {
currentFramerate: framerate,
currentLanguage: language,
desktopShareFramerates: SS_SUPPORTED_FRAMERATES,
followMeActive: Boolean(conference && followMeActive),
followMeEnabled: Boolean(conference && followMeEnabled),
languages: LANGUAGES,
showLanguageSettings: configuredTabs.includes('language'),
showModeratorSettings,
showPrejoinSettings: state['features/base/config'].prejoinPageEnabled,
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),
startVideoMuted: Boolean(conference && startVideoMutedPolicy)
};
@ -178,6 +185,7 @@ export function getSoundsTabProps(stateful: Object | Function) {
soundsReactions
} = state['features/base/settings'];
const enableReactions = isReactionsEnabled(state);
const moderatorMutedSoundsReactions = state['features/base/conference'].startReactionsMuted ?? false;
return {
soundsIncomingMessage,
@ -185,7 +193,8 @@ export function getSoundsTabProps(stateful: Object | Function) {
soundsParticipantLeft,
soundsTalkWhileMuted,
soundsReactions,
enableReactions
enableReactions,
moderatorMutedSoundsReactions
};
}

View File

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