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.
pull/2250/head
Leonard Kim 7 years ago committed by yanas
parent 0eafee2a95
commit c9b54845d9
  1. 16
      conference.js
  2. 61
      css/_side_toolbar_container.scss
  3. 7
      modules/UI/UI.js
  4. 253
      modules/UI/side_pannels/settings/SettingsMenu.js
  5. 23
      modules/translation/translation.js
  6. 23
      react/features/base/conference/actionTypes.js
  7. 74
      react/features/base/conference/actions.js
  8. 17
      react/features/base/conference/reducer.js
  9. 0
      react/features/settings-menu/components/DeviceSelectionButton.native.js
  10. 87
      react/features/settings-menu/components/DeviceSelectionButton.web.js
  11. 0
      react/features/settings-menu/components/LanguageSelectDropdown.native.js
  12. 179
      react/features/settings-menu/components/LanguageSelectDropdown.web.js
  13. 0
      react/features/settings-menu/components/ModeratorCheckboxes.native.js
  14. 199
      react/features/settings-menu/components/ModeratorCheckboxes.web.js
  15. 0
      react/features/settings-menu/components/SettingsMenu.native.js
  16. 108
      react/features/settings-menu/components/SettingsMenu.web.js
  17. 1
      react/features/settings-menu/components/index.js
  18. 1
      react/features/settings-menu/index.js
  19. 6
      service/UI/UIEvents.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

@ -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%;
.settings-menu {
display: flex;
flex-direction: column;
padding-left: 10%;
padding-right: 10%;
.moderator-checkbox {
display: inline-block;
margin: 0 5px 0;
width: auto;
}
}
}
#device_settings {
width : auto !important;
text-align: center;
}
.moderator-option {
margin-top: 15px;
}
#deviceOptionsWrapper {
button {
float: none;
.subTitle {
color: $defaultSideBarFontColor;
font-size: 11px;
font-weight: 500;
}
}
}

@ -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.
*

@ -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 */
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 = `
<div id="settings_container" class="sideToolbarContainer__inner">
<div class="title" data-i18n="settings.title"></div>
<form class="aui">
<div id="languagesSelectWrapper"
class="sideToolbarBlock first hide">
<select id="languagesSelect"></select>
</div>
<div id="deviceOptionsWrapper" class="hide">
<div id="deviceOptionsTitle" class="subTitle hide"
data-i18n="settings.audioVideo"></div>
<div class="sideToolbarBlock first">
<button
class="${deviceSelectionButtonClasses}"
data-i18n="deviceSelection.deviceSettings"
id="deviceSelection"
type="button"></button>
</div>
</div>
<div id="moderatorOptionsWrapper" class="hide">
<div id="moderatorOptionsTitle" class="subTitle hide"
data-i18n="settings.moderator"></div>
<div id="startMutedOptions" class="hide">
<div class="sideToolbarBlock first">
<input type="checkbox" id="startAudioMuted">
<label class="startMutedLabel" for="startAudioMuted"
data-i18n="settings.startAudioMuted"></label>
</div>
<div class="sideToolbarBlock">
<input type="checkbox" id="startVideoMuted">
<label class="startMutedLabel" for="startVideoMuted"
data-i18n="settings.startVideoMuted"></label>
</div>
</div>
<div id="followMeOptions" class="hide">
<div class="sideToolbarBlock">
<input type="checkbox" id="followMeCheckBox">
<label class="followMeLabel" for="followMeCheckBox"
data-i18n="settings.followMe"></label>
</div>
</div>
</div>
</form>
</div>`;
/**
*
*/
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);
/* 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 */
return `<option ${attrsStr}></option>`;
}).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);
}
}
import UIUtil from '../../util/UIUtil';
export default {
init(emitter) {
initHTML();
// LANGUAGES BOX
if (UIUtil.isSettingEnabled('language')) {
const wrapperId = 'languagesSelectWrapper';
const selectId = 'languagesSelect';
const selectEl = AJS.$(`#${selectId}`);
let selectInput; // eslint-disable-line prefer-const
selectEl.html(generateLanguagesOptions(
LANGUAGES,
APP.translation.getCurrentLanguage()
));
initSelect2(selectEl, () => {
const val = selectEl.val();
selectInput[0].dataset.i18n = `languages:${val}`;
APP.translation.translateElement(selectInput);
emitter.emit(UIEvents.LANG_CHANGED, val);
});
// 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';
init() {
const settingsMenuContainer = document.createElement('div');
$('#deviceSelection').on('click', () =>
APP.store.dispatch(openDeviceSelectionDialog()));
settingsMenuContainer.id = 'settings_container';
settingsMenuContainer.className = 'sideToolbarContainer__inner';
// Only show the subtitle if this isn't the only setting section.
if (interfaceConfig.SETTINGS_SECTIONS.length > 1) {
UIUtil.setVisible('deviceOptionsTitle', true);
}
$('#sideToolbarContainer').append(settingsMenuContainer);
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);
},
const props = {
showDeviceSettings: UIUtil.isSettingEnabled('devices'),
showLanguageSettings: UIUtil.isSettingEnabled('language'),
showModeratorSettings: UIUtil.isSettingEnabled('moderator'),
showTitles: interfaceConfig.SETTINGS_SECTIONS.length > 1
};
/**
* 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(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<SettingsMenu
{ ...props } />
</I18nextProvider>
</Provider>,
settingsMenuContainer
);
},
/**

@ -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 `<span data-i18n="${key}"${optAttr}>${text}</span>`;
}
/**
*
*/
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);
}
/**

@ -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');

@ -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.
*

@ -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;

@ -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 (
<div>
{ this.props.showTitle
? <div className = 'subTitle'>
{ this.props.t('settings.audioVideo') }
</div>
: null }
<Button
appearance = 'primary'
onClick = { this._onOpenDeviceSelectionDialog }
shouldFitContainer = { true }>
{ this.props.t('deviceSelection.deviceSettings') }
</Button>
</div>
);
}
/**
* Opens the {@code DeviceSelectionDialog}.
*
* @private
* @returns {void}
*/
_onOpenDeviceSelectionDialog() {
this.props.dispatch(openDeviceSelectionDialog());
}
}
export default translate(connect()(DeviceSelectionButton));

@ -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
<DropdownItem
key = { language }
// eslint-disable-next-line react/jsx-no-bind
onClick = { () => this._onLanguageSelected(language) }>
{ t(`languages:${language}`) }
</DropdownItem>
);
return (
<div>
<DropdownMenu
isOpen = { this.state.isLanguageSelectOpen }
onOpenChange = { this._onSetDropdownOpen }
shouldFitContainer = { true }
trigger = { currentLanguage
? t(`languages:${currentLanguage}`)
: '' }
triggerButtonProps = {{
appearance: 'primary',
shouldFitContainer: true
}}
triggerType = 'button'>
<DropdownItemGroup>
{ languageItems }
</DropdownItemGroup>
</DropdownMenu>
</div>
);
}
/**
* 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);

@ -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 (
<div>
{ showTitle
? <div className = 'subTitle'>
{ t('settings.moderator') }
</div>
: null }
<div className = 'moderator-option'>
<input
checked = { _startAudioMutedPolicy }
className = 'moderator-checkbox'
id = 'startAudioMuted'
onChange = { this._onSetStartAudioMutedPolicy }
type = 'checkbox' />
<label
className = 'moderator-checkbox-label'
htmlFor = 'startAudioMuted'>
{ t('settings.startAudioMuted') }
</label>
</div>
<div className = 'moderator-option'>
<input
checked = { _startVideoMutedPolicy }
className = 'moderator-checkbox'
id = 'startVideoMuted'
onChange = { this._onSetStartVideoMutedPolicy }
type = 'checkbox' />
<label
className = 'moderator-checkbox-label'
htmlFor = 'startVideoMuted'>
{ t('settings.startVideoMuted') }
</label>
</div>
<div className = 'moderator-option'>
<input
checked = { _followMeEnabled }
className = 'moderator-checkbox'
id = 'followMeCheckBox'
onChange = { this._onSetFollowMeSetting }
type = 'checkbox' />
<label
className = 'moderator-checkbox-label'
htmlFor = 'followMeCheckBox'>
{ t('settings.followMe') }
</label>
</div>
</div>
);
}
/**
* 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));

@ -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 (
<div className = 'settings-menu'>
<div className = 'title'>
{ t('settings.title') }
</div>
{ showLanguageSettings
? <LanguageSelectDropdown />
: null }
{ showDeviceSettings
? <DeviceSelectionButton showTitle = { showTitles } />
: null }
{ _isModerator && showModeratorSettings
? <ModeratorCheckboxes showTitle = { showTitles } />
: null }
</div>
);
}
}
/**
* 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));

@ -0,0 +1 @@
export { default as SettingsMenu } from './SettingsMenu';

@ -0,0 +1 @@
export * from './components';

@ -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',

Loading…
Cancel
Save