From c9b54845d9677e5075e0cc8f5df5a8d70d2e2d90 Mon Sep 17 00:00:00 2001 From: Leonard Kim Date: Mon, 20 Nov 2017 18:21:35 -0800 Subject: [PATCH] ref(settings): convert panel to react The goal is to reduce usage on atlassian/aui. New components have been created to display the settings panel. Language selection will reach into i18n for state whereas moderator options will keep state in redux. --- conference.js | 16 +- css/_side_toolbar_container.scss | 63 ++--- modules/UI/UI.js | 7 - .../UI/side_pannels/settings/SettingsMenu.js | 255 +++--------------- modules/translation/translation.js | 23 +- react/features/base/conference/actionTypes.js | 23 ++ react/features/base/conference/actions.js | 74 ++++- react/features/base/conference/reducer.js | 17 +- .../DeviceSelectionButton.native.js | 0 .../components/DeviceSelectionButton.web.js | 87 ++++++ .../LanguageSelectDropdown.native.js | 0 .../components/LanguageSelectDropdown.web.js | 179 ++++++++++++ .../components/ModeratorCheckboxes.native.js | 0 .../components/ModeratorCheckboxes.web.js | 199 ++++++++++++++ .../components/SettingsMenu.native.js | 0 .../components/SettingsMenu.web.js | 108 ++++++++ .../settings-menu/components/index.js | 1 + react/features/settings-menu/index.js | 1 + service/UI/UIEvents.js | 6 - 19 files changed, 748 insertions(+), 311 deletions(-) create mode 100644 react/features/settings-menu/components/DeviceSelectionButton.native.js create mode 100644 react/features/settings-menu/components/DeviceSelectionButton.web.js create mode 100644 react/features/settings-menu/components/LanguageSelectDropdown.native.js create mode 100644 react/features/settings-menu/components/LanguageSelectDropdown.web.js create mode 100644 react/features/settings-menu/components/ModeratorCheckboxes.native.js create mode 100644 react/features/settings-menu/components/ModeratorCheckboxes.web.js create mode 100644 react/features/settings-menu/components/SettingsMenu.native.js create mode 100644 react/features/settings-menu/components/SettingsMenu.web.js create mode 100644 react/features/settings-menu/components/index.js create mode 100644 react/features/settings-menu/index.js 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',