diff --git a/conference.js b/conference.js
index 125d4b3b5..9fe20db93 100644
--- a/conference.js
+++ b/conference.js
@@ -29,6 +29,7 @@ import {
dataChannelOpened,
EMAIL_COMMAND,
lockStateChanged,
+ onStartMutedPolicyChanged,
p2pStatusChanged,
sendLocalParticipant
} from './react/features/base/conference';
@@ -2078,18 +2079,11 @@ export default {
APP.UI.addListener(UIEvents.NICKNAME_CHANGED,
this.changeLocalDisplayName.bind(this));
- APP.UI.addListener(UIEvents.START_MUTED_CHANGED,
- (startAudioMuted, startVideoMuted) => {
- room.setStartMutedPolicy({
- audio: startAudioMuted,
- video: startVideoMuted
- });
- }
- );
room.on(
JitsiConferenceEvents.START_MUTED_POLICY_CHANGED,
({ audio, video }) => {
- APP.UI.onStartMutedChanged(audio, video);
+ APP.store.dispatch(
+ onStartMutedPolicyChanged(audio, video));
}
);
room.on(JitsiConferenceEvents.STARTED_MUTED, () => {
@@ -2373,10 +2367,6 @@ export default {
APP.UI.initConference();
- APP.UI.addListener(
- UIEvents.LANG_CHANGED,
- language => APP.translation.setLanguage(language));
-
APP.keyboardshortcut.init();
if (config.requireDisplayName
diff --git a/css/_side_toolbar_container.scss b/css/_side_toolbar_container.scss
index e44d4f597..e9c27b419 100644
--- a/css/_side_toolbar_container.scss
+++ b/css/_side_toolbar_container.scss
@@ -22,8 +22,10 @@
/**
* Form elements and blocks.
*/
- input, select, a,
- .sideToolbarBlock, .form-control, .button-control {
+ input,
+ a,
+ .sideToolbarBlock,
+ .form-control {
display: block;
margin-top: 15px;
margin-left: 10%;
@@ -34,19 +36,11 @@
* Specify styling of elements inside a block.
*/
.sideToolbarBlock {
- input, button, a, select {
+ input, a {
margin-left: 0;
margin-top: 5px;
width: 100%;
}
- input[type='checkbox'] {
- display: inline;
- width: auto !important;
- > label {
- margin-top: 5px;
- width: 80%;
- }
- }
}
/**
@@ -80,42 +74,35 @@
font-size: $toolbarTitleFontSize;
}
- /**
- * Subtitle specific properties.
- */
- div.subTitle {
- color: $defaultSideBarFontColor !important;
- font-size: 11px;
- font-weight: 500;
- margin-left: 10%;
- text-align: left;
- }
-
/**
* First element after a title.
*/
.first {
margin-top: 0 !important;
}
-
- /**
- * Buttons in the side toolbar container.
- */
- .button-control {
- margin: 9px 0;
- width: 100%;
- }
}
-}
-#device_settings {
- width : auto !important;
- text-align: center;
-}
+ .settings-menu {
+ display: flex;
+ flex-direction: column;
+ padding-left: 10%;
+ padding-right: 10%;
-#deviceOptionsWrapper {
- button {
- float: none;
+ .moderator-checkbox {
+ display: inline-block;
+ margin: 0 5px 0;
+ width: auto;
+ }
+
+ .moderator-option {
+ margin-top: 15px;
+ }
+
+ .subTitle {
+ color: $defaultSideBarFontColor;
+ font-size: 11px;
+ font-weight: 500;
+ }
}
}
diff --git a/modules/UI/UI.js b/modules/UI/UI.js
index f00e18e6f..2358cc52f 100644
--- a/modules/UI/UI.js
+++ b/modules/UI/UI.js
@@ -17,7 +17,6 @@ import Recording from './recording/Recording';
import VideoLayout from './videolayout/VideoLayout';
import Filmstrip from './videolayout/Filmstrip';
-import SettingsMenu from './side_pannels/settings/SettingsMenu';
import Profile from './side_pannels/profile/Profile';
import {
@@ -541,8 +540,6 @@ UI.updateLocalRole = isModerator => {
APP.store.dispatch(showSharedVideoButton());
Recording.showRecordingButton(isModerator);
- SettingsMenu.showStartMutedOptions(isModerator);
- SettingsMenu.showFollowMeOptions(isModerator);
if (isModerator) {
if (!interfaceConfig.DISABLE_FOCUS_INDICATOR) {
@@ -1017,10 +1014,6 @@ UI.updateAuthInfo = function(isAuthEnabled, login) {
}
};
-UI.onStartMutedChanged = function(startAudioMuted, startVideoMuted) {
- SettingsMenu.updateStartMutedBox(startAudioMuted, startVideoMuted);
-};
-
/**
* Notifies interested listeners that the raise hand property has changed.
*
diff --git a/modules/UI/side_pannels/settings/SettingsMenu.js b/modules/UI/side_pannels/settings/SettingsMenu.js
index 76a35a1d0..35d2106c7 100644
--- a/modules/UI/side_pannels/settings/SettingsMenu.js
+++ b/modules/UI/side_pannels/settings/SettingsMenu.js
@@ -1,235 +1,42 @@
-/* global $, APP, AJS, interfaceConfig */
-import { LANGUAGES } from '../../../../react/features/base/i18n';
-import { openDeviceSelectionDialog }
- from '../../../../react/features/device-selection';
+/* global $, APP, interfaceConfig */
+
+/* eslint-disable no-unused-vars */
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { I18nextProvider } from 'react-i18next';
+import { Provider } from 'react-redux';
+
+import { i18next } from '../../../../react/features/base/i18n';
+import { SettingsMenu } from '../../../../react/features/settings-menu';
+/* eslint-enable no-unused-vars */
import UIUtil from '../../util/UIUtil';
-import UIEvents from '../../../../service/UI/UIEvents';
-
-const sidePanelsContainerId = 'sideToolbarContainer';
-const deviceSelectionButtonClasses
- = 'button-control button-control_primary button-control_full-width';
-const htmlStr = `
-
`;
-
-/**
- *
- */
-function initHTML() {
- $(`#${sidePanelsContainerId}`)
- .append(htmlStr);
-
- // make sure we translate the panel, as adding it can be after i18n
- // library had initialized and translated already present html
- APP.translation.translateElement($(`#${sidePanelsContainerId}`));
-}
-
-/**
- * Generate html select options for available languages.
- *
- * @param {string[]} items available languages
- * @param {string} [currentLang] current language
- * @returns {string}
- */
-function generateLanguagesOptions(items, currentLang) {
- return items.map(lang => {
- const attrs = {
- value: lang,
- 'data-i18n': `languages:${lang}`
- };
-
- if (lang === currentLang) {
- attrs.selected = 'selected';
- }
-
- const attrsStr = UIUtil.attrsToString(attrs);
-
-
- return ``;
- }).join('');
-}
-
-/**
- * Replace html select element to select2 custom dropdown
- *
- * @param {jQueryElement} $el native select element
- * @param {function} onSelectedCb fired if item is selected
- */
-function initSelect2($el, onSelectedCb) {
- $el.auiSelect2({
- minimumResultsForSearch: Infinity
- });
- if (typeof onSelectedCb === 'function') {
- $el.change(onSelectedCb);
- }
-}
export default {
- init(emitter) {
- initHTML();
+ init() {
+ const settingsMenuContainer = document.createElement('div');
- // LANGUAGES BOX
- if (UIUtil.isSettingEnabled('language')) {
- const wrapperId = 'languagesSelectWrapper';
- const selectId = 'languagesSelect';
- const selectEl = AJS.$(`#${selectId}`);
- let selectInput; // eslint-disable-line prefer-const
+ settingsMenuContainer.id = 'settings_container';
+ settingsMenuContainer.className = 'sideToolbarContainer__inner';
- selectEl.html(generateLanguagesOptions(
- LANGUAGES,
- APP.translation.getCurrentLanguage()
- ));
- initSelect2(selectEl, () => {
- const val = selectEl.val();
+ $('#sideToolbarContainer').append(settingsMenuContainer);
- selectInput[0].dataset.i18n = `languages:${val}`;
- APP.translation.translateElement(selectInput);
- emitter.emit(UIEvents.LANG_CHANGED, val);
- });
+ const props = {
+ showDeviceSettings: UIUtil.isSettingEnabled('devices'),
+ showLanguageSettings: UIUtil.isSettingEnabled('language'),
+ showModeratorSettings: UIUtil.isSettingEnabled('moderator'),
+ showTitles: interfaceConfig.SETTINGS_SECTIONS.length > 1
+ };
- // find new selectInput element
- selectInput = $(`#s2id_${selectId} .select2-chosen`);
-
- // first select fix for languages options
- selectInput[0].dataset.i18n
- = `languages:${APP.translation.getCurrentLanguage()}`;
-
- // translate selectInput, which is the currently selected language
- // otherwise there will be no selected option
- APP.translation.translateElement(selectInput);
- APP.translation.translateElement(selectEl);
-
- APP.translation.addLanguageChangedListener(
- lng => {
- selectInput[0].dataset.i18n = `languages:${lng}`;
- });
-
- UIUtil.setVisible(wrapperId, true);
- }
-
- // DEVICES LIST
- if (UIUtil.isSettingEnabled('devices')) {
- const wrapperId = 'deviceOptionsWrapper';
-
- $('#deviceSelection').on('click', () =>
- APP.store.dispatch(openDeviceSelectionDialog()));
-
- // Only show the subtitle if this isn't the only setting section.
- if (interfaceConfig.SETTINGS_SECTIONS.length > 1) {
- UIUtil.setVisible('deviceOptionsTitle', true);
- }
-
- UIUtil.setVisible(wrapperId, true);
- }
-
- // MODERATOR
- if (UIUtil.isSettingEnabled('moderator')) {
- const wrapperId = 'moderatorOptionsWrapper';
-
- // START MUTED
- $('#startMutedOptions').change(() => {
- const startAudioMuted = $('#startAudioMuted').is(':checked');
- const startVideoMuted = $('#startVideoMuted').is(':checked');
-
- emitter.emit(
- UIEvents.START_MUTED_CHANGED,
- startAudioMuted,
- startVideoMuted
- );
- });
-
- // FOLLOW ME
- const followMeToggle = document.getElementById('followMeCheckBox');
-
- followMeToggle.addEventListener('change', () => {
- const isFollowMeEnabled = followMeToggle.checked;
-
- emitter.emit(UIEvents.FOLLOW_ME_ENABLED, isFollowMeEnabled);
- });
-
- UIUtil.setVisible(wrapperId, true);
- }
- },
-
- /**
- * If start audio muted/start video muted options should be visible or not.
- * @param {boolean} show
- */
- showStartMutedOptions(show) {
- if (show && UIUtil.isSettingEnabled('moderator')) {
- // Only show the subtitle if this isn't the only setting section.
- if (!$('#moderatorOptionsTitle').is(':visible')
- && interfaceConfig.SETTINGS_SECTIONS.length > 1) {
- UIUtil.setVisible('moderatorOptionsTitle', true);
- }
-
- UIUtil.setVisible('startMutedOptions', true);
- } else {
- // Only show the subtitle if this isn't the only setting section.
- if ($('#moderatorOptionsTitle').is(':visible')) {
- UIUtil.setVisible('moderatorOptionsTitle', false);
- }
-
- UIUtil.setVisible('startMutedOptions', false);
- }
- },
-
- updateStartMutedBox(startAudioMuted, startVideoMuted) {
- $('#startAudioMuted').attr('checked', startAudioMuted);
- $('#startVideoMuted').attr('checked', startVideoMuted);
- },
-
- /**
- * Shows/hides the follow me options in the settings dialog.
- *
- * @param {boolean} show {true} to show those options, {false} to hide them
- */
- showFollowMeOptions(show) {
- UIUtil.setVisible(
- 'followMeOptions',
- show && UIUtil.isSettingEnabled('moderator'));
+ ReactDOM.render(
+
+
+
+
+ ,
+ settingsMenuContainer
+ );
},
/**
diff --git a/modules/translation/translation.js b/modules/translation/translation.js
index 460b0f263..64510f44d 100644
--- a/modules/translation/translation.js
+++ b/modules/translation/translation.js
@@ -2,7 +2,7 @@
import jqueryI18next from 'jquery-i18next';
-import { DEFAULT_LANGUAGE, i18next } from '../../react/features/base/i18n';
+import { i18next } from '../../react/features/base/i18n';
declare var $: Function;
@@ -20,13 +20,6 @@ function _onI18nInitialized() {
*
*/
class Translation {
- /**
- *
- */
- addLanguageChangedListener(listener: Function) {
- i18next.on('languageChanged', listener);
- }
-
/**
*
*/
@@ -40,13 +33,6 @@ class Translation {
return `${text}`;
}
- /**
- *
- */
- getCurrentLanguage() {
- return i18next.lng();
- }
-
/**
*
*/
@@ -58,13 +44,8 @@ class Translation {
} else {
i18next.on('initialized', _onI18nInitialized);
}
- }
- /**
- *
- */
- setLanguage(language: string = DEFAULT_LANGUAGE) {
- i18next.setLng(language, {}, _onI18nInitialized);
+ i18next.on('languageChanged', _onI18nInitialized);
}
/**
diff --git a/react/features/base/conference/actionTypes.js b/react/features/base/conference/actionTypes.js
index 9688a5a71..67845a6b0 100644
--- a/react/features/base/conference/actionTypes.js
+++ b/react/features/base/conference/actionTypes.js
@@ -96,6 +96,17 @@ export const P2P_STATUS_CHANGED = Symbol('P2P_STATUS_CHANGED');
*/
export const SET_AUDIO_ONLY = Symbol('SET_AUDIO_ONLY');
+/**
+ * The type of (redux) action which updates the current known status of the
+ * Follow Me feature.
+ *
+ * {
+ * type: SET_FOLLOW_ME,
+ * enabled: boolean
+ * }
+ */
+export const SET_FOLLOW_ME = Symbol('SET_FOLLOW_ME');
+
/**
* The type of (redux) action which sets the video channel's lastN (value).
*
@@ -162,3 +173,15 @@ export const SET_ROOM = Symbol('SET_ROOM');
* }
*/
export const SET_SIP_GATEWAY_ENABLED = Symbol('SET_SIP_GATEWAY_ENABLED');
+
+/**
+ * The type of (redux) action which updates the current known status of the
+ * moderator features for starting participants as audio or video muted.
+ *
+ * {
+ * type: SET_START_MUTED_POLICY,
+ * startAudioMutedPolicy: boolean,
+ * startVideoMutedPolicy: boolean
+ * }
+ */
+export const SET_START_MUTED_POLICY = Symbol('SET_START_MUTED_POLICY');
diff --git a/react/features/base/conference/actions.js b/react/features/base/conference/actions.js
index 00180c452..08d27d285 100644
--- a/react/features/base/conference/actions.js
+++ b/react/features/base/conference/actions.js
@@ -1,5 +1,7 @@
// @flow
+import UIEvents from '../../../../service/UI/UIEvents';
+
import { sendAnalyticsEvent } from '../../analytics';
import { getName } from '../../app';
import { JitsiConferenceEvents } from '../lib-jitsi-meet';
@@ -24,11 +26,13 @@ import {
LOCK_STATE_CHANGED,
P2P_STATUS_CHANGED,
SET_AUDIO_ONLY,
+ SET_FOLLOW_ME,
SET_LASTN,
SET_PASSWORD,
SET_PASSWORD_FAILED,
SET_RECEIVE_VIDEO_QUALITY,
- SET_ROOM
+ SET_ROOM,
+ SET_START_MUTED_POLICY
} from './actionTypes';
import {
AVATAR_ID_COMMAND,
@@ -45,6 +49,8 @@ import type { Dispatch } from 'redux';
const logger = require('jitsi-meet-logger').getLogger(__filename);
+declare var APP: Object;
+
/**
* Adds conference (event) listeners.
*
@@ -362,6 +368,28 @@ export function lockStateChanged(conference: Object, locked: boolean) {
};
}
+/**
+ * Updates the known state of start muted policies.
+ *
+ * @param {boolean} audioMuted - Whether or not members will join the conference
+ * as audio muted.
+ * @param {boolean} videoMuted - Whether or not members will join the conference
+ * as video muted.
+ * @returns {{
+ * type: SET_START_MUTED_POLICY,
+ * startAudioMutedPolicy: boolean,
+ * startVideoMutedPolicy: boolean
+ * }}
+ */
+export function onStartMutedPolicyChanged(
+ audioMuted: boolean, videoMuted: boolean) {
+ return {
+ type: SET_START_MUTED_POLICY,
+ startAudioMutedPolicy: audioMuted,
+ startVideoMutedPolicy: videoMuted
+ };
+}
+
/**
* Sets whether or not peer2peer is currently enabled.
*
@@ -395,6 +423,26 @@ export function setAudioOnly(audioOnly: boolean) {
};
}
+/**
+ * Enables or disables the Follow Me feature.
+ *
+ * @param {boolean} enabled - Whether or not Follow Me should be enabled.
+ * @returns {{
+ * type: SET_FOLLOW_ME,
+ * enabled: boolean
+ * }}
+ */
+export function setFollowMe(enabled: boolean) {
+ if (typeof APP !== 'undefined') {
+ APP.UI.emitEvent(UIEvents.FOLLOW_ME_ENABLED, enabled);
+ }
+
+ return {
+ type: SET_FOLLOW_ME,
+ enabled
+ };
+}
+
/**
* Sets the video channel's last N (value) of the current conference. A value of
* undefined shall be used to reset it to the default value.
@@ -528,6 +576,30 @@ export function setRoom(room: ?string) {
};
}
+/**
+ * Sets whether or not members should join audio and/or video muted.
+ *
+ * @param {boolean} startAudioMuted - Whether or not members will join the
+ * conference as audio muted.
+ * @param {boolean} startVideoMuted - Whether or not members will join the
+ * conference as video muted.
+ * @returns {Function}
+ */
+export function setStartMutedPolicy(
+ startAudioMuted: boolean, startVideoMuted: boolean) {
+ return (dispatch: Dispatch<*>, getState: Function) => {
+ const { conference } = getState()['features/base/conference'];
+
+ conference.setStartMutedPolicy({
+ audio: startAudioMuted,
+ video: startVideoMuted
+ });
+
+ return dispatch(
+ onStartMutedPolicyChanged(startAudioMuted, startVideoMuted));
+ };
+}
+
/**
* Toggles the audio-only flag for the current JitsiConference.
*
diff --git a/react/features/base/conference/reducer.js b/react/features/base/conference/reducer.js
index 1b8604dd7..b02cf4ef8 100644
--- a/react/features/base/conference/reducer.js
+++ b/react/features/base/conference/reducer.js
@@ -14,10 +14,12 @@ import {
LOCK_STATE_CHANGED,
P2P_STATUS_CHANGED,
SET_AUDIO_ONLY,
+ SET_FOLLOW_ME,
SET_PASSWORD,
SET_RECEIVE_VIDEO_QUALITY,
SET_ROOM,
- SET_SIP_GATEWAY_ENABLED
+ SET_SIP_GATEWAY_ENABLED,
+ SET_START_MUTED_POLICY
} from './actionTypes';
import { VIDEO_QUALITY_LEVELS } from './constants';
import { isRoomValid } from './functions';
@@ -55,6 +57,12 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
case SET_AUDIO_ONLY:
return _setAudioOnly(state, action);
+ case SET_FOLLOW_ME:
+ return {
+ ...state,
+ followMeEnabled: action.enabled
+ };
+
case SET_PASSWORD:
return _setPassword(state, action);
@@ -66,6 +74,13 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
case SET_SIP_GATEWAY_ENABLED:
return _setSIPGatewayEnabled(state, action);
+
+ case SET_START_MUTED_POLICY:
+ return {
+ ...state,
+ startAudioMutedPolicy: action.startAudioMutedPolicy,
+ startVideoMutedPolicy: action.startVideoMutedPolicy
+ };
}
return state;
diff --git a/react/features/settings-menu/components/DeviceSelectionButton.native.js b/react/features/settings-menu/components/DeviceSelectionButton.native.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/react/features/settings-menu/components/DeviceSelectionButton.web.js b/react/features/settings-menu/components/DeviceSelectionButton.web.js
new file mode 100644
index 000000000..0ba5c230e
--- /dev/null
+++ b/react/features/settings-menu/components/DeviceSelectionButton.web.js
@@ -0,0 +1,87 @@
+import Button from '@atlaskit/button';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+
+import { translate } from '../../base/i18n';
+import { openDeviceSelectionDialog } from '../../device-selection';
+
+/**
+ * Implements a React {@link Component} which displays a button for opening the
+ * {@code DeviceSelectionDialog}.
+ *
+ * @extends Component
+ */
+class DeviceSelectionButton extends Component {
+ /**
+ * {@code DeviceSelectionButton} component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ /**
+ * Invoked to display the {@code DeviceSelectionDialog}.
+ */
+ dispatch: PropTypes.func,
+
+ /**
+ * Whether or not the button's title should be displayed.
+ */
+ showTitle: PropTypes.bool,
+
+ /**
+ * Invoked to obtain translated strings.
+ */
+ t: PropTypes.func
+ };
+
+ /**
+ * Initializes a new {@code DeviceSelectionButton} instance.
+ *
+ * @param {Object} props - The read-only properties with which the new
+ * instance is to be initialized.
+ */
+ constructor(props) {
+ super(props);
+
+ // Bind event handler so it is only bound once for every instance.
+ this._onOpenDeviceSelectionDialog
+ = this._onOpenDeviceSelectionDialog.bind(this);
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ return (
+
+ { this.props.showTitle
+ ?
+ { this.props.t('settings.audioVideo') }
+
+ : null }
+
+
+ );
+ }
+
+ /**
+ * Opens the {@code DeviceSelectionDialog}.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onOpenDeviceSelectionDialog() {
+ this.props.dispatch(openDeviceSelectionDialog());
+ }
+}
+
+export default translate(connect()(DeviceSelectionButton));
diff --git a/react/features/settings-menu/components/LanguageSelectDropdown.native.js b/react/features/settings-menu/components/LanguageSelectDropdown.native.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/react/features/settings-menu/components/LanguageSelectDropdown.web.js b/react/features/settings-menu/components/LanguageSelectDropdown.web.js
new file mode 100644
index 000000000..5118b6c06
--- /dev/null
+++ b/react/features/settings-menu/components/LanguageSelectDropdown.web.js
@@ -0,0 +1,179 @@
+import DropdownMenu, {
+ DropdownItem,
+ DropdownItemGroup
+} from '@atlaskit/dropdown-menu';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+
+import { DEFAULT_LANGUAGE, LANGUAGES, translate } from '../../base/i18n';
+
+/**
+ * Implements a React {@link Component} which displays a dropdown for changing
+ * application text to another language.
+ *
+ * @extends Component
+ */
+class LanguageSelectDropdown extends Component {
+ /**
+ * {@code LanguageSelectDropdown} component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ /**
+ * The translation service.
+ */
+ i18n: PropTypes.object,
+
+ /**
+ * Invoked to obtain translated strings.
+ */
+ t: PropTypes.func
+ };
+
+
+ /**
+ * {@code LanguageSelectDropdown} component's local state.
+ *
+ * @type {Object}
+ * @property {string|null} currentLanguage - The currently selected language
+ * the application should be displayed in.
+ * @property {boolean} isLanguageSelectOpen - Whether or not the dropdown
+ * should be displayed as open.
+ */
+ state = {
+ currentLanguage: null,
+ isLanguageSelectOpen: false
+ };
+
+ /**
+ * Initializes a new {@code LanguageSelectDropdown} instance.
+ *
+ * @param {Object} props - The read-only properties with which the new
+ * instance is to be initialized.
+ */
+ constructor(props) {
+ super(props);
+
+ this.state.currentLanguage
+ = this.props.i18n.language || DEFAULT_LANGUAGE;
+
+ // Bind event handlers so they are only bound once for every instance.
+ this._onLanguageSelected = this._onLanguageSelected.bind(this);
+ this._onSetDropdownOpen = this._onSetDropdownOpen.bind(this);
+ this._setCurrentLanguage = this._setCurrentLanguage.bind(this);
+ }
+
+ /**
+ * Sets a listener to update the currently selected language if it is
+ * changed from somewhere else.
+ *
+ * @inheritdoc
+ * @returns {void}
+ */
+ componentDidMount() {
+ this.props.i18n.on('languageChanged', this._setCurrentLanguage);
+ }
+
+ /**
+ * Removes all listeners.
+ *
+ * @inheritdoc
+ * @returns {void}
+ */
+ componentWillUnmount() {
+ this.props.i18n.off('languageChanged', this._setCurrentLanguage);
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const { t } = this.props;
+ const { currentLanguage } = this.state;
+
+ const languageItems = LANGUAGES.map(language =>
+ // eslint-disable-next-line react/jsx-wrap-multilines
+ this._onLanguageSelected(language) }>
+ { t(`languages:${language}`) }
+
+ );
+
+ return (
+
+
+
+ { languageItems }
+
+
+
+ );
+ }
+
+ /**
+ * Updates the application's currently displayed language.
+ *
+ * @param {string} language - The language code for the language to display.
+ * @private
+ * @returns {void}
+ */
+ _onLanguageSelected(language) {
+ const previousLanguage = this.state.currentLanguage;
+
+ this.setState({
+ currentLanguage: language,
+ isLanguageSelectOpen: false
+ });
+
+ this.props.i18n.changeLanguage(language, error => {
+ if (error) {
+ this._setCurrentLanguage(previousLanguage);
+ }
+ });
+ }
+
+ /**
+ * Set whether or not the dropdown should be open.
+ *
+ * @param {Object} dropdownEvent - The event returned from requesting the
+ * open state of the dropdown be changed.
+ * @private
+ * @returns {void}
+ */
+ _onSetDropdownOpen(dropdownEvent) {
+ this.setState({
+ isLanguageSelectOpen: dropdownEvent.isOpen
+ });
+ }
+
+ /**
+ * Updates the known current language of the application.
+ *
+ * @param {string} currentLanguage - The language code for the current
+ * language.
+ * @private
+ * @returns {void}
+ */
+ _setCurrentLanguage(currentLanguage) {
+ this.setState({ currentLanguage });
+ }
+}
+
+export default translate(LanguageSelectDropdown);
diff --git a/react/features/settings-menu/components/ModeratorCheckboxes.native.js b/react/features/settings-menu/components/ModeratorCheckboxes.native.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/react/features/settings-menu/components/ModeratorCheckboxes.web.js b/react/features/settings-menu/components/ModeratorCheckboxes.web.js
new file mode 100644
index 000000000..e3db3c0f6
--- /dev/null
+++ b/react/features/settings-menu/components/ModeratorCheckboxes.web.js
@@ -0,0 +1,199 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+
+import { setFollowMe, setStartMutedPolicy } from '../../base/conference';
+import { translate } from '../../base/i18n';
+
+/**
+ * Implements a React {@link Component} which displays checkboxes for enabling
+ * and disabling moderator-only conference features.
+ *
+ * @extends Component
+ */
+class ModeratorCheckboxes extends Component {
+ /**
+ * {@code ModeratorCheckboxes} component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ /**
+ * Whether or not the Follow Me feature is currently enabled.
+ */
+ _followMeEnabled: PropTypes.bool,
+
+ /**
+ * Whether or not new members will join the conference as audio muted.
+ */
+ _startAudioMutedPolicy: PropTypes.bool,
+
+ /**
+ * Whether or note new member will join the conference as video muted.
+ */
+ _startVideoMutedPolicy: PropTypes.bool,
+
+ /**
+ * Invoked to enable and disable moderator-only conference features.
+ */
+ dispatch: PropTypes.func,
+
+ /**
+ * Whether or not the title should be displayed.
+ */
+ showTitle: PropTypes.bool,
+
+ /**
+ * Invokted to obtain translated strings.
+ */
+ t: PropTypes.func
+ };
+
+ /**
+ * Initializes a new {@code ModeratorCheckboxes} instance.
+ *
+ * @param {Object} props - The read-only properties with which the new
+ * instance is to be initialized.
+ */
+ constructor(props) {
+ super(props);
+
+ // Bind event handlers so they are only bound once for every instance.
+ this._onSetFollowMeSetting
+ = this._onSetFollowMeSetting.bind(this);
+ this._onSetStartAudioMutedPolicy
+ = this._onSetStartAudioMutedPolicy.bind(this);
+ this._onSetStartVideoMutedPolicy
+ = this._onSetStartVideoMutedPolicy.bind(this);
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const {
+ _followMeEnabled,
+ _startAudioMutedPolicy,
+ _startVideoMutedPolicy,
+ showTitle,
+ t
+ } = this.props;
+
+ return (
+
+ { showTitle
+ ?
+ { t('settings.moderator') }
+
+ : null }
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ /**
+ * Toggles the Follow Me feature.
+ *
+ * @param {Object} event - The dom event returned from changes the checkbox.
+ * @private
+ * @returns {void}
+ */
+ _onSetFollowMeSetting(event) {
+ this.props.dispatch(setFollowMe(event.target.checked));
+ }
+
+ /**
+ * Toggles whether or not new members should join the conference as audio
+ * muted.
+ *
+ * @param {Object} event - The dom event returned from changes the checkbox.
+ * @private
+ * @returns {void}
+ */
+ _onSetStartAudioMutedPolicy(event) {
+ this.props.dispatch(setStartMutedPolicy(
+ event.target.checked, this.props._startVideoMutedPolicy));
+ }
+
+ /**
+ * Toggles whether or not new members should join the conference as video
+ * muted.
+ *
+ * @param {Object} event - The dom event returned from changes the checkbox.
+ * @private
+ * @returns {void}
+ */
+ _onSetStartVideoMutedPolicy(event) {
+ this.props.dispatch(setStartMutedPolicy(
+ this.props._startAudioMutedPolicy, event.target.checked));
+ }
+}
+
+/**
+ * Maps (parts of) the Redux state to the associated props for the
+ * {@code ModeratorCheckboxes} component.
+ *
+ * @param {Object} state - The Redux state.
+ * @private
+ * @returns {{
+ * _followMeEnabled: boolean,
+ * _startAudioMutedPolicy: boolean,
+ * _startVideoMutedPolicy: boolean
+ * }}
+ */
+function _mapStateToProps(state) {
+ const {
+ followMeEnabled,
+ startAudioMutedPolicy,
+ startVideoMutedPolicy
+ } = state['features/base/conference'];
+
+ return {
+ _followMeEnabled: Boolean(followMeEnabled),
+ _startAudioMutedPolicy: Boolean(startAudioMutedPolicy),
+ _startVideoMutedPolicy: Boolean(startVideoMutedPolicy)
+ };
+}
+
+export default translate(connect(_mapStateToProps)(ModeratorCheckboxes));
diff --git a/react/features/settings-menu/components/SettingsMenu.native.js b/react/features/settings-menu/components/SettingsMenu.native.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/react/features/settings-menu/components/SettingsMenu.web.js b/react/features/settings-menu/components/SettingsMenu.web.js
new file mode 100644
index 000000000..4b4888e3d
--- /dev/null
+++ b/react/features/settings-menu/components/SettingsMenu.web.js
@@ -0,0 +1,108 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+
+import { translate } from '../../base/i18n';
+import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
+
+import DeviceSelectionButton from './DeviceSelectionButton';
+import LanguageSelectDropdown from './LanguageSelectDropdown';
+import ModeratorCheckboxes from './ModeratorCheckboxes';
+
+/**
+ * Implements a React {@link Component} which various ways to change application
+ * settings.
+ *
+ * @extends Component
+ */
+class SettingsMenu extends Component {
+ /**
+ * {@code SettingsMenu} component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ /**
+ * Whether or not the local user is a moderator.
+ */
+ _isModerator: PropTypes.bool,
+
+ /**
+ * Whether or not the button to open device selection should display.
+ */
+ showDeviceSettings: PropTypes.bool,
+
+ /**
+ * Whether or not the dropdown to change the current translated language
+ * should display.
+ */
+ showLanguageSettings: PropTypes.bool,
+
+ /**
+ * Whether or not moderator-only actions that affect the conference
+ * should display.
+ */
+ showModeratorSettings: PropTypes.bool,
+
+ /**
+ * Whether or not menu section should have section titles displayed.
+ */
+ showTitles: PropTypes.bool,
+
+ /**
+ * Invoked to obtain translated strings.
+ */
+ t: PropTypes.func
+ };
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const {
+ _isModerator,
+ showDeviceSettings,
+ showLanguageSettings,
+ showModeratorSettings,
+ showTitles,
+ t
+ } = this.props;
+
+ return (
+
+
+ { t('settings.title') }
+
+ { showLanguageSettings
+ ?
+ : null }
+ { showDeviceSettings
+ ?
+ : null }
+ { _isModerator && showModeratorSettings
+ ?
+ : null }
+
+ );
+ }
+}
+
+/**
+ * Maps parts of Redux store to component prop types.
+ *
+ * @param {Object} state - Snapshot of Redux store.
+ * @returns {{
+ * _isModerator: boolean
+ * }}
+ */
+function _mapStateToProps(state) {
+ return {
+ _isModerator:
+ getLocalParticipant(state).role === PARTICIPANT_ROLE.MODERATOR
+ };
+}
+
+export default translate(connect(_mapStateToProps)(SettingsMenu));
diff --git a/react/features/settings-menu/components/index.js b/react/features/settings-menu/components/index.js
new file mode 100644
index 000000000..d01f80b4c
--- /dev/null
+++ b/react/features/settings-menu/components/index.js
@@ -0,0 +1 @@
+export { default as SettingsMenu } from './SettingsMenu';
diff --git a/react/features/settings-menu/index.js b/react/features/settings-menu/index.js
new file mode 100644
index 000000000..07635cbbc
--- /dev/null
+++ b/react/features/settings-menu/index.js
@@ -0,0 +1 @@
+export * from './components';
diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js
index 47e18f200..dcaed5720 100644
--- a/service/UI/UIEvents.js
+++ b/service/UI/UIEvents.js
@@ -8,11 +8,6 @@ export default {
*/
MESSAGE_CREATED: 'UI.message_created',
- /**
- * Notifies that local user changed language.
- */
- LANG_CHANGED: 'UI.lang_changed',
-
/**
* Notifies that local user changed email.
*/
@@ -21,7 +16,6 @@ export default {
/**
* Notifies that "start muted" settings changed.
*/
- START_MUTED_CHANGED: 'UI.start_muted_changed',
AUDIO_MUTED: 'UI.audio_muted',
VIDEO_MUTED: 'UI.video_muted',
VIDEO_UNMUTING_WHILE_AUDIO_ONLY: 'UI.video_unmuting_while_audio_only',