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:
parent
0eafee2a95
commit
c9b54845d9
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 */
|
||||
|
||||
/* 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
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -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…
Reference in New Issue