diff --git a/lang/main.json b/lang/main.json index 0c8d94165..8e0113709 100644 --- a/lang/main.json +++ b/lang/main.json @@ -995,6 +995,7 @@ "more": "More", "name": "Name", "noDevice": "None", + "notifications": "Notifications", "participantJoined": "Participant Joined", "participantKnocking": "Participant entered lobby", "participantLeft": "Participant Left", @@ -1005,7 +1006,6 @@ "selectCamera": "Camera", "selectMic": "Microphone", "selfView": "Self view", - "sounds": "Sounds", "speakers": "Speakers", "startAudioMuted": "Everyone starts muted", "startReactionsMuted": "Mute reaction sounds for everyone", diff --git a/react/features/settings/actions.ts b/react/features/settings/actions.ts index 7d2554a30..0310c93b7 100644 --- a/react/features/settings/actions.ts +++ b/react/features/settings/actions.ts @@ -21,8 +21,8 @@ import { LogoutDialog, SettingsDialog } from './components'; import { getModeratorTabProps, getMoreTabProps, - getProfileTabProps, - getSoundsTabProps + getNotificationsTabProps, + getProfileTabProps } from './functions'; /** @@ -96,17 +96,6 @@ export function submitMoreTab(newState: any) { })); } - const enabledNotifications = newState.enabledNotifications; - - if (enabledNotifications !== currentState.enabledNotifications) { - dispatch(updateSettings({ - userSelectedNotifications: { - ...getState()['features/base/settings'].userSelectedNotifications, - ...enabledNotifications - } - })); - } - if (newState.currentFramerate !== currentState.currentFramerate) { const frameRate = parseInt(newState.currentFramerate, 10); @@ -183,9 +172,9 @@ export function submitProfileTab(newState: any) { * @param {Object} newState - The new settings. * @returns {Function} */ -export function submitSoundsTab(newState: any) { +export function submitNotificationsTab(newState: any) { return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { - const currentState = getSoundsTabProps(getState()); + const currentState = getNotificationsTabProps(getState()); const shouldNotUpdateReactionSounds = getModeratorTabProps(getState()).startReactionsMuted; const shouldUpdate = (newState.soundsIncomingMessage !== currentState.soundsIncomingMessage) || (newState.soundsParticipantJoined !== currentState.soundsParticipantJoined) @@ -209,6 +198,17 @@ export function submitSoundsTab(newState: any) { } dispatch(updateSettings(settingsToUpdate)); } + + const enabledNotifications = newState.enabledNotifications; + + if (enabledNotifications !== currentState.enabledNotifications) { + dispatch(updateSettings({ + userSelectedNotifications: { + ...getState()['features/base/settings'].userSelectedNotifications, + ...enabledNotifications + } + })); + } }; } diff --git a/react/features/settings/components/web/MoreTab.tsx b/react/features/settings/components/web/MoreTab.tsx index 83e71be6d..7ed78145f 100644 --- a/react/features/settings/components/web/MoreTab.tsx +++ b/react/features/settings/components/web/MoreTab.tsx @@ -27,11 +27,6 @@ export type Props = AbstractDialogTabProps & WithTranslation & { */ desktopShareFramerates: Array; - /** - * The types of enabled notifications that can be configured and their specific visibility. - */ - enabledNotifications: Object; - /** * Whether or not follow me is currently active (enabled by some other participant). */ @@ -47,11 +42,6 @@ export type Props = AbstractDialogTabProps & WithTranslation & { */ showModeratorSettings: boolean; - /** - * Whether or not to display notifications settings. - */ - showNotificationsSettings: boolean; - /** * Whether or not to show prejoin screen. */ @@ -90,7 +80,6 @@ class MoreTab extends AbstractDialogTab { // Bind event handler so it is only bound once for every instance. this._onFramerateItemSelect = this._onFramerateItemSelect.bind(this); - this._onEnabledNotificationsChanged = this._onEnabledNotificationsChanged.bind(this); this._onShowPrejoinPageChanged = this._onShowPrejoinPageChanged.bind(this); this._onKeyboardShortcutEnableChanged = this._onKeyboardShortcutEnableChanged.bind(this); this._renderMaxStageParticipantsSelect = this._renderMaxStageParticipantsSelect.bind(this); @@ -143,24 +132,6 @@ class MoreTab extends AbstractDialogTab { super._onChange({ showPrejoinPage: checked }); } - /** - * Callback invoked to select if the given type of - * notifications should be shown. - * - * @param {Object} e - The key event to handle. - * @param {string} type - The type of the notification. - * - * @returns {void} - */ - _onEnabledNotificationsChanged({ target: { checked } }: React.ChangeEvent, type: any) { - super._onChange({ - enabledNotifications: { - ...this.props.enabledNotifications, - [type]: checked - } - }); - } - /** * Callback invoked to select if global keyboard shortcuts * should be enabled. @@ -269,37 +240,6 @@ class MoreTab extends AbstractDialogTab { ); } - /** - * Returns the React Element for modifying the enabled notifications settings. - * - * @private - * @returns {ReactElement} - */ - _renderNotificationsSettings() { - const { t, enabledNotifications } = this.props; - - return ( -
- - { t('notify.displayNotifications') } - - { - Object.keys(enabledNotifications).map(key => ( - this._onEnabledNotificationsChanged(e, key) } /> - )) - } -
- ); - } - /** * Returns the React Element for the max stage participants dropdown. * @@ -357,14 +297,13 @@ class MoreTab extends AbstractDialogTab { * @returns {ReactElement} */ _renderSettingsLeft() { - const { showNotificationsSettings, showPrejoinSettings } = this.props; + const { showPrejoinSettings } = this.props; return (
{ showPrejoinSettings && this._renderPrejoinScreenSettings() } - { showNotificationsSettings && this._renderNotificationsSettings() } { this._renderKeyboardShortcutCheckbox() }
); diff --git a/react/features/settings/components/web/NotificationsTab.tsx b/react/features/settings/components/web/NotificationsTab.tsx new file mode 100644 index 000000000..1de527f27 --- /dev/null +++ b/react/features/settings/components/web/NotificationsTab.tsx @@ -0,0 +1,276 @@ +import { Theme } from '@mui/material'; +import { withStyles } from '@mui/styles'; +import React from 'react'; +import { WithTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; + +import { IReduxState } from '../../../app/types'; +import AbstractDialogTab, { + IProps as AbstractDialogTabProps } from '../../../base/dialog/components/web/AbstractDialogTab'; +import { translate } from '../../../base/i18n/functions'; +import { withPixelLineHeight } from '../../../base/styles/functions.web'; +import Checkbox from '../../../base/ui/components/web/Checkbox'; + +/** + * The type of the React {@code Component} props of {@link NotificationsTab}. + */ +export interface IProps extends AbstractDialogTabProps, WithTranslation { + + /** + * CSS classes object. + */ + classes: any; + + /** + * Array of disabled sounds ids. + */ + disabledSounds: string[]; + + /** + * Whether or not the reactions feature is enabled. + */ + enableReactions: Boolean; + + /** + * The types of enabled notifications that can be configured and their specific visibility. + */ + enabledNotifications: Object; + + /** + * Whether or not moderator muted the sounds. + */ + moderatorMutedSoundsReactions: Boolean; + + /** + * Whether or not to display notifications settings. + */ + showNotificationsSettings: boolean; + + /** + * Whether sound settings should be displayed or not. + */ + showSoundsSettings: boolean; + + /** + * Whether or not the sound for the incoming message should play. + */ + soundsIncomingMessage: Boolean; + + /** + * Whether or not the sound for the participant joined should play. + */ + soundsParticipantJoined: Boolean; + + /** + * Whether or not the sound for the participant entering the lobby should play. + */ + soundsParticipantKnocking: Boolean; + + /** + * Whether or not the sound for the participant left should play. + */ + soundsParticipantLeft: Boolean; + + /** + * Whether or not the sound for reactions should play. + */ + soundsReactions: Boolean; + + /** + * Whether or not the sound for the talk while muted notification should play. + */ + soundsTalkWhileMuted: Boolean; +} + +const styles = (theme: Theme) => { + return { + container: { + display: 'flex', + width: '100%' + }, + + column: { + flex: 1, + + '&:first-child:not(:last-child)': { + marginRight: theme.spacing(3) + } + }, + + title: { + ...withPixelLineHeight(theme.typography.heading6), + color: `${theme.palette.text01} !important`, + marginBottom: theme.spacing(3) + }, + + checkbox: { + marginBottom: theme.spacing(3) + } + }; +}; + +/** + * React {@code Component} for modifying the local user's sound settings. + * + * @augments Component + */ +class NotificationsTab extends AbstractDialogTab { + /** + * Initializes a new {@code SoundsTab} instance. + * + * @param {IProps} props - The React {@code Component} props to initialize + * the new {@code SoundsTab} instance with. + */ + constructor(props: IProps) { + super(props); + + // Bind event handlers so they are only bound once for every instance. + this._onChange = this._onChange.bind(this); + this._onEnabledNotificationsChanged = this._onEnabledNotificationsChanged.bind(this); + } + + /** + * Changes a sound setting state. + * + * @param {Object} e - The key event to handle. + * + * @returns {void} + */ + _onChange({ target }: React.ChangeEvent) { + super._onChange({ [target.name]: target.checked }); + } + + /** + * Callback invoked to select if the given type of + * notifications should be shown. + * + * @param {Object} e - The key event to handle. + * @param {string} type - The type of the notification. + * + * @returns {void} + */ + _onEnabledNotificationsChanged({ target: { checked } }: React.ChangeEvent, type: any) { + super._onChange({ + enabledNotifications: { + ...this.props.enabledNotifications, + [type]: checked + } + }); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { + classes, + disabledSounds, + enabledNotifications, + showNotificationsSettings, + showSoundsSettings, + soundsIncomingMessage, + soundsParticipantJoined, + soundsParticipantKnocking, + soundsParticipantLeft, + soundsTalkWhileMuted, + soundsReactions, + enableReactions, + moderatorMutedSoundsReactions, + t + } = this.props; + + return ( +
+ {showSoundsSettings && ( +
+

+ {t('settings.playSounds')} +

+ {enableReactions && + } + + + + + +
+ )} + {showNotificationsSettings && ( +
+

+ {t('notify.displayNotifications')} +

+ { + Object.keys(enabledNotifications).map(key => ( + this._onEnabledNotificationsChanged(e, key) } /> + )) + } +
+ )} +
+ ); + } +} + +const mapStateToProps = (_state: IReduxState) => { + const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || []; + const showSoundsSettings = configuredTabs.includes('sounds'); + + return { + showSoundsSettings + }; +}; + +export default connect(mapStateToProps)(withStyles(styles)(translate(NotificationsTab))); diff --git a/react/features/settings/components/web/SettingsDialog.tsx b/react/features/settings/components/web/SettingsDialog.tsx index 124c1e06f..aa4a07aca 100644 --- a/react/features/settings/components/web/SettingsDialog.tsx +++ b/react/features/settings/components/web/SettingsDialog.tsx @@ -18,23 +18,24 @@ import { import { submitModeratorTab, submitMoreTab, - submitProfileTab, - submitSoundsTab + submitNotificationsTab, + submitProfileTab } from '../../actions'; import { SETTINGS_TABS } from '../../constants'; import { getModeratorTabProps, getMoreTabProps, - getProfileTabProps, - getSoundsTabProps + getNotificationsMap, + getNotificationsTabProps, + getProfileTabProps } from '../../functions'; // @ts-ignore import CalendarTab from './CalendarTab'; import ModeratorTab from './ModeratorTab'; import MoreTab from './MoreTab'; +import NotificationsTab from './NotificationsTab'; import ProfileTab from './ProfileTab'; -import SoundsTab from './SoundsTab'; /* eslint-enable lines-around-comment */ /** @@ -148,7 +149,7 @@ const styles = (theme: Theme) => { '& .settings-checkbox': { display: 'flex', - marginBottom: theme.spacing(2) + marginBottom: theme.spacing(3) }, '& .calendar-tab': { @@ -248,6 +249,8 @@ function _mapStateToProps(state: IReduxState, ownProps: any) { const showCalendarSettings = configuredTabs.includes('calendar') && isCalendarEnabled(state); const showSoundsSettings = configuredTabs.includes('sounds'); + const enabledNotifications = getNotificationsMap(state); + const showNotificationsSettings = Object.keys(enabledNotifications).length > 0; const tabs: IDialogTab[] = []; if (showDeviceSettings) { @@ -276,6 +279,24 @@ function _mapStateToProps(state: IReduxState, ownProps: any) { }); } + if (showSoundsSettings || showNotificationsSettings) { + tabs.push({ + name: SETTINGS_TABS.NOTIFICATIONS, + component: NotificationsTab, + labelKey: 'settings.notifications', + propsUpdateFunction: (tabState: any, newProps: any) => { + return { + ...newProps, + enabledNotifications: tabState?.enabledNotifications || {} + }; + }, + props: getNotificationsTabProps(state), + className: `settings-pane ${classes.settingsDialog}`, + submit: submitNotificationsTab, + icon: IconBell + }); + } + if (showModeratorSettings) { tabs.push({ name: SETTINGS_TABS.MODERATOR, @@ -321,18 +342,6 @@ function _mapStateToProps(state: IReduxState, ownProps: any) { }); } - if (showSoundsSettings) { - tabs.push({ - name: SETTINGS_TABS.SOUNDS, - component: SoundsTab, - labelKey: 'settings.sounds', - props: getSoundsTabProps(state), - className: `settings-pane ${classes.settingsDialog} profile-pane`, - submit: submitSoundsTab, - icon: IconBell - }); - } - if (showMoreTab) { tabs.push({ name: SETTINGS_TABS.MORE, @@ -350,7 +359,6 @@ function _mapStateToProps(state: IReduxState, ownProps: any) { currentLanguage: tabState?.currentLanguage, hideSelfView: tabState?.hideSelfView, showPrejoinPage: tabState?.showPrejoinPage, - enabledNotifications: tabState?.enabledNotifications || {}, maxStageParticipants: tabState?.maxStageParticipants }; }, diff --git a/react/features/settings/components/web/SoundsTab.tsx b/react/features/settings/components/web/SoundsTab.tsx deleted file mode 100644 index efc99063d..000000000 --- a/react/features/settings/components/web/SoundsTab.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import React from 'react'; -import { WithTranslation } from 'react-i18next'; - -// @ts-ignore -import { AbstractDialogTab } from '../../../base/dialog'; -// eslint-disable-next-line lines-around-comment -// @ts-ignore -import type { Props as AbstractDialogTabProps } from '../../../base/dialog'; -import { translate } from '../../../base/i18n/functions'; -import Checkbox from '../../../base/ui/components/web/Checkbox'; - -/** - * The type of the React {@code Component} props of {@link SoundsTab}. - */ -export type Props = AbstractDialogTabProps & WithTranslation & { - - /** - * Whether or not the reactions feature is enabled. - */ - enableReactions: Boolean; - - /** - * Whether or not moderator muted the sounds. - */ - moderatorMutedSoundsReactions: Boolean; - - /** - * Whether or not the sound for the incoming message should play. - */ - soundsIncomingMessage: Boolean; - - /** - * Whether or not the sound for the participant joined should play. - */ - soundsParticipantJoined: Boolean; - - /** - * Whether or not the sound for the participant entering the lobby should play. - */ - soundsParticipantKnocking: Boolean; - - /** - * Whether or not the sound for the participant left should play. - */ - soundsParticipantLeft: Boolean; - - /** - * Whether or not the sound for reactions should play. - */ - soundsReactions: Boolean; - - /** - * Whether or not the sound for the talk while muted notification should play. - */ - soundsTalkWhileMuted: Boolean; - - /** - * Invoked to obtain translated strings. - */ - t: Function; -}; - -/** - * React {@code Component} for modifying the local user's sound settings. - * - * @augments Component - */ -class SoundsTab extends AbstractDialogTab { - /** - * Initializes a new {@code SoundsTab} instance. - * - * @param {Props} props - The React {@code Component} props to initialize - * the new {@code SoundsTab} instance with. - */ - constructor(props: Props) { - super(props); - - // Bind event handlers so they are only bound once for every instance. - this._onChange = this._onChange.bind(this); - } - - /** - * Changes a sound setting state. - * - * @param {Object} e - The key event to handle. - * - * @returns {void} - */ - _onChange({ target }: React.ChangeEvent) { - super._onChange({ [target.name]: target.checked }); - } - - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - const { - disabledSounds, - soundsIncomingMessage, - soundsParticipantJoined, - soundsParticipantKnocking, - soundsParticipantLeft, - soundsTalkWhileMuted, - soundsReactions, - enableReactions, - moderatorMutedSoundsReactions, - t // @ts-ignore - } = this.props; - - return ( -
-

- {t('settings.playSounds')} -

- {enableReactions && - } - - - - - -
- ); - } -} - -// @ts-ignore -export default translate(SoundsTab); diff --git a/react/features/settings/constants.ts b/react/features/settings/constants.ts index d8cb233f6..5c4739432 100644 --- a/react/features/settings/constants.ts +++ b/react/features/settings/constants.ts @@ -3,8 +3,8 @@ export const SETTINGS_TABS = { DEVICES: 'devices_tab', MORE: 'more_tab', MODERATOR: 'moderator-tab', - PROFILE: 'profile_tab', - SOUNDS: 'sounds_tab' + NOTIFICATIONS: 'notifications_tab', + PROFILE: 'profile_tab' }; /** diff --git a/react/features/settings/functions.any.ts b/react/features/settings/functions.any.ts index e5568fc18..ca6619da4 100644 --- a/react/features/settings/functions.any.ts +++ b/react/features/settings/functions.any.ts @@ -116,14 +116,11 @@ export function getNotificationsMap(stateful: IStateful) { export function getMoreTabProps(stateful: IStateful) { const state = toState(stateful); const framerate = state['features/screen-share'].captureFrameRate ?? SS_DEFAULT_FRAME_RATE; - const enabledNotifications = getNotificationsMap(stateful); const stageFilmstripEnabled = isStageFilmstripEnabled(state); return { currentFramerate: framerate, desktopShareFramerates: SS_SUPPORTED_FRAMERATES, - enabledNotifications, - showNotificationsSettings: Object.keys(enabledNotifications).length > 0, showPrejoinPage: !state['features/base/settings'].userSelectedSkipPrejoin, showPrejoinSettings: state['features/base/config'].prejoinConfig?.enabled, maxStageParticipants: state['features/base/settings'].maxStageParticipants, @@ -228,7 +225,7 @@ export function getProfileTabProps(stateful: IStateful) { * @returns {Object} - The properties for the "Sounds" tab from settings * dialog. */ -export function getSoundsTabProps(stateful: IStateful) { +export function getNotificationsTabProps(stateful: IStateful) { const state = toState(stateful); const { soundsIncomingMessage, @@ -240,9 +237,12 @@ export function getSoundsTabProps(stateful: IStateful) { } = state['features/base/settings']; const enableReactions = isReactionsEnabled(state); const moderatorMutedSoundsReactions = state['features/base/conference'].startReactionsMuted ?? false; + const enabledNotifications = getNotificationsMap(stateful); return { disabledSounds: state['features/base/config'].disabledSounds || [], + enabledNotifications, + showNotificationsSettings: Object.keys(enabledNotifications).length > 0, soundsIncomingMessage, soundsParticipantJoined, soundsParticipantKnocking,