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.
This commit is contained in:
Leonard Kim 2017-11-20 18:21:35 -08:00 committed by yanas
parent 0eafee2a95
commit c9b54845d9
19 changed files with 748 additions and 311 deletions

View File

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

View File

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

View File

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

View File

@ -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 = `
<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);
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);
}
}
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(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<SettingsMenu
{ ...props } />
</I18nextProvider>
</Provider>,
settingsMenuContainer
);
},
/**

View File

@ -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);
}
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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