Refactor settings modal (#3121)

* feat(settings): setting dialog

- Move device selection, profile edit, language select, moderator
  options, and server auth into one modal with tabs.
- Remove side panel profile and settings and logic used to update
  them.
- Pipe server auth status into redux to display in the settings
  dialog.
- Change filmstrip only device selection popup to use the new
  stateless settings dialog component.

* squash: do not show profile tab if not guest

* squash: profile button not clickable if no profile to show

* squash: nits

* ref: Settings dialog.
This commit is contained in:
Hristo Terezov 2018-06-20 15:19:53 -05:00 committed by virtuacoplenny
parent 0acc9187ed
commit 1f8fa3b6d4
49 changed files with 1703 additions and 1841 deletions

View File

@ -33,6 +33,7 @@ import EventEmitter from 'events';
import { import {
AVATAR_ID_COMMAND, AVATAR_ID_COMMAND,
AVATAR_URL_COMMAND, AVATAR_URL_COMMAND,
authStatusChanged,
conferenceFailed, conferenceFailed,
conferenceJoined, conferenceJoined,
conferenceLeft, conferenceLeft,
@ -1650,7 +1651,7 @@ export default {
room.on( room.on(
JitsiConferenceEvents.AUTH_STATUS_CHANGED, JitsiConferenceEvents.AUTH_STATUS_CHANGED,
(authEnabled, authLogin) => (authEnabled, authLogin) =>
APP.UI.updateAuthInfo(authEnabled, authLogin)); APP.store.dispatch(authStatusChanged(authEnabled, authLogin)));
room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED, room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED,
user => APP.UI.onUserFeaturesChanged(user)); user => APP.UI.onUserFeaturesChanged(user));
@ -1997,7 +1998,6 @@ export default {
id: from, id: from,
email: data.value email: data.value
})); }));
APP.UI.setUserEmail(from, data.value);
}); });
room.addCommandListener( room.addCommandListener(
@ -2575,7 +2575,6 @@ export default {
email: formattedEmail email: formattedEmail
})); }));
APP.UI.setUserEmail(localId, formattedEmail);
sendData(commands.EMAIL, formattedEmail); sendData(commands.EMAIL, formattedEmail);
}, },

View File

@ -92,7 +92,7 @@
/** /**
* Titles and subtitles of inner containers. * Titles and subtitles of inner containers.
*/ */
div.title, div.subTitle { div.title {
margin: 24px 0 11px; margin: 24px 0 11px;
} }
@ -112,53 +112,4 @@
margin-top: 0 !important; margin-top: 0 !important;
} }
} }
.settings-menu {
display: flex;
flex-direction: column;
padding-left: 10%;
padding-right: 10%;
.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;
}
}
}
/**
* Profile
*/
.auth_container {
ul {
padding: 0;
li {
list-style-type: none;
a.authButton {
width: 160px;
margin: 10px 20px;
padding: 3px 29px;
box-sizing: border-box;
background-color: #06a5df;
border-radius: 4px;
cursor: pointer;
color: $defaultColor;
text-decoration: none;
text-align: center;
}
}
}
} }

View File

@ -41,6 +41,7 @@
@import 'modals/dialog'; @import 'modals/dialog';
@import 'modals/feedback/feedback'; @import 'modals/feedback/feedback';
@import 'modals/invite/info'; @import 'modals/invite/info';
@import 'modals/settings/settings';
@import 'modals/speaker_stats/speaker_stats'; @import 'modals/speaker_stats/speaker_stats';
@import 'modals/video-quality/video-quality'; @import 'modals/video-quality/video-quality';
@import 'videolayout_default'; @import 'videolayout_default';

View File

@ -0,0 +1,43 @@
.settings-pane {
display: flex;
width: 100%;
&.profile-pane {
flex-direction: column;
}
.auth-name {
margin-bottom: 4px;
}
.device-selection {
margin-top: 20px;
}
.mock-atlaskit-label {
color: #56637A;
font-size: 12px;
font-weight: 600;
line-height: 1.33;
padding: 20px 0px 4px 0px;
}
.more-tab,
.profile-edit {
display: flex;
width: 100%;
}
.profile-edit-field,
.settings-sub-pane {
flex: 1;
}
.profile-edit-field {
margin-right: 20px;
}
.language-settings {
max-width: 50%;
}
}

View File

@ -51,7 +51,7 @@ var interfaceConfig = {
'invite', 'feedback', 'stats', 'shortcuts' 'invite', 'feedback', 'stats', 'shortcuts'
], ],
SETTINGS_SECTIONS: [ 'language', 'devices', 'moderator' ], SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile' ],
// Determines how the video would fit the screen. 'both' would fit the whole // Determines how the video would fit the screen. 'both' would fit the whole
// screen, 'height' would fit the original video height to the height of the // screen, 'height' would fit the original video height to the height of the

View File

@ -163,11 +163,15 @@
"selectMic": "Microphone", "selectMic": "Microphone",
"selectAudioOutput": "Audio output", "selectAudioOutput": "Audio output",
"followMe": "Everyone follows me", "followMe": "Everyone follows me",
"language": "Language",
"loggedIn": "Logged in as __name__",
"noDevice": "None", "noDevice": "None",
"cameraAndMic": "Camera and microphone", "cameraAndMic": "Camera and microphone",
"moderator": "MODERATOR", "moderator": "Moderator",
"more": "More",
"password": "SET PASSWORD", "password": "SET PASSWORD",
"audioVideo": "AUDIO AND VIDEO" "audioVideo": "AUDIO AND VIDEO",
"devices": "Devices"
}, },
"profile": { "profile": {
"title": "Profile", "title": "Profile",

View File

@ -15,11 +15,7 @@ import SharedVideoManager from './shared_video/SharedVideo';
import VideoLayout from './videolayout/VideoLayout'; import VideoLayout from './videolayout/VideoLayout';
import Filmstrip from './videolayout/Filmstrip'; import Filmstrip from './videolayout/Filmstrip';
import Profile from './side_pannels/profile/Profile';
import {
openDeviceSelectionDialog
} from '../../react/features/device-selection';
import { updateDeviceList } from '../../react/features/base/devices'; import { updateDeviceList } from '../../react/features/base/devices';
import { JitsiTrackErrors } from '../../react/features/base/lib-jitsi-meet'; import { JitsiTrackErrors } from '../../react/features/base/lib-jitsi-meet';
import { import {
@ -33,7 +29,6 @@ import {
setNotificationsEnabled, setNotificationsEnabled,
showWarningNotification showWarningNotification
} from '../../react/features/notifications'; } from '../../react/features/notifications';
import { shouldShowOnlyDeviceSelection } from '../../react/features/settings';
import { import {
dockToolbox, dockToolbox,
setToolboxEnabled, setToolboxEnabled,
@ -97,22 +92,6 @@ const UIListeners = new Map([
], [ ], [
UIEvents.TOGGLE_CHAT, UIEvents.TOGGLE_CHAT,
() => UI.toggleChat() () => UI.toggleChat()
], [
UIEvents.TOGGLE_SETTINGS,
() => {
// Opening of device selection is special-cased as it is a dialog
// opened through a button in settings and not directly displayed in
// settings itself. As it is not useful to only have a settings menu
// with a button to open a dialog, open the dialog directly instead.
if (shouldShowOnlyDeviceSelection()) {
APP.store.dispatch(openDeviceSelectionDialog());
} else {
UI.toggleSidePanel('settings_container');
}
}
], [
UIEvents.TOGGLE_PROFILE,
() => UI.toggleSidePanel('profile_container')
], [ ], [
UIEvents.TOGGLE_FILMSTRIP, UIEvents.TOGGLE_FILMSTRIP,
() => UI.handleToggleFilmstrip() () => UI.handleToggleFilmstrip()
@ -216,7 +195,6 @@ UI.changeDisplayName = function(id, displayName) {
VideoLayout.onDisplayNameChanged(id, displayName); VideoLayout.onDisplayNameChanged(id, displayName);
if (APP.conference.isLocalId(id) || id === 'localVideoContainer') { if (APP.conference.isLocalId(id) || id === 'localVideoContainer') {
Profile.changeDisplayName(displayName);
Chat.setChatConversationMode(Boolean(displayName)); Chat.setChatConversationMode(Boolean(displayName));
} }
}; };
@ -268,7 +246,7 @@ UI.setLocalRaisedHandStatus
*/ */
UI.initConference = function() { UI.initConference = function() {
const { getState } = APP.store; const { getState } = APP.store;
const { email, id, name } = getLocalParticipant(getState); const { id, name } = getLocalParticipant(getState);
// Update default button states before showing the toolbar // Update default button states before showing the toolbar
// if local role changes buttons state will be again updated. // if local role changes buttons state will be again updated.
@ -282,11 +260,6 @@ UI.initConference = function() {
UI.changeDisplayName('localVideoContainer', displayName); UI.changeDisplayName('localVideoContainer', displayName);
} }
// Make sure we configure our avatar id, before creating avatar for us
if (email) {
UI.setUserEmail(id, email);
}
// FollowMe attempts to copy certain aspects of the moderator's UI into the // FollowMe attempts to copy certain aspects of the moderator's UI into the
// other participants' UI. Consequently, it needs (1) read and write access // other participants' UI. Consequently, it needs (1) read and write access
// to the UI (depending on the moderator role of the local participant) and // to the UI (depending on the moderator role of the local participant) and
@ -492,9 +465,6 @@ UI.addUser = function(user) {
APP.store.dispatch(showParticipantJoinedNotification(displayName)); APP.store.dispatch(showParticipantJoinedNotification(displayName));
} }
// Configure avatar
UI.setUserEmail(id);
// set initial display name // set initial display name
if (displayName) { if (displayName) {
UI.changeDisplayName(id, displayName); UI.changeDisplayName(id, displayName);
@ -739,17 +709,6 @@ UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout));
// Used by torture. // Used by torture.
UI.dockToolbar = dock => APP.store.dispatch(dockToolbox(dock)); UI.dockToolbar = dock => APP.store.dispatch(dockToolbox(dock));
/**
* Update user email.
* @param {string} id user id
* @param {string} email user email
*/
UI.setUserEmail = function(id, email) {
if (APP.conference.isLocalId(id)) {
Profile.changeEmail(email);
}
};
/** /**
* Updates the displayed avatar for participant. * Updates the displayed avatar for participant.
* *
@ -880,25 +839,6 @@ UI.notifyFocusDisconnected = function(focus, retrySec) {
); );
}; };
/**
* Updates auth info on the UI.
* @param {boolean} isAuthEnabled if authentication is enabled
* @param {string} [login] current login
*/
UI.updateAuthInfo = function(isAuthEnabled, login) {
const showAuth = isAuthEnabled && UIUtil.isAuthenticationEnabled();
const loggedIn = Boolean(login);
Profile.showAuthenticationButtons(showAuth);
if (showAuth) {
Profile.setAuthenticatedIdentity(login);
Profile.showLoginButton(!loggedIn);
Profile.showLogoutButton(loggedIn);
}
};
/** /**
* Notifies interested listeners that the raise hand property has changed. * Notifies interested listeners that the raise hand property has changed.
* *

View File

@ -1,6 +1,4 @@
import Chat from './chat/Chat'; import Chat from './chat/Chat';
import SettingsMenu from './settings/SettingsMenu';
import Profile from './profile/Profile';
import { isButtonEnabled } from '../../../react/features/toolbox'; import { isButtonEnabled } from '../../../react/features/toolbox';
const SidePanels = { const SidePanels = {
@ -9,16 +7,6 @@ const SidePanels = {
if (isButtonEnabled('chat')) { if (isButtonEnabled('chat')) {
Chat.init(eventEmitter); Chat.init(eventEmitter);
} }
// Initialize settings
if (isButtonEnabled('settings')) {
SettingsMenu.init(eventEmitter);
}
// Initialize profile
if (isButtonEnabled('profile')) {
Profile.init(eventEmitter);
}
} }
}; };

View File

@ -43,6 +43,10 @@ const htmlStr = `
function initHTML() { function initHTML() {
$(`#${sidePanelsContainerId}`) $(`#${sidePanelsContainerId}`)
.append(htmlStr); .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}`));
} }
/** /**

View File

@ -1,194 +0,0 @@
/* global $, APP */
import UIUtil from '../../util/UIUtil';
import UIEvents from '../../../../service/UI/UIEvents';
import {
createProfilePanelButtonEvent,
sendAnalytics
} from '../../../../react/features/analytics';
const sidePanelsContainerId = 'sideToolbarContainer';
const htmlStr = `
<div id='profile_container' class='sideToolbarContainer__inner'>
<div class='title' data-i18n='profile.title'></div>
<div class='sideToolbarBlock first'>
<label class='first' data-i18n='profile.setDisplayNameLabel'>
</label>
<input class='input-control' type='text' id='setDisplayName'
data-i18n='[placeholder]settings.name'>
</div>
<div class='sideToolbarBlock'>
<label data-i18n='profile.setEmailLabel'></label>
<input id='setEmail' type='text' class='input-control'
data-i18n='[placeholder]profile.setEmailInput'>
</div>
<div id='profile_auth_container'
class='sideToolbarBlock auth_container'>
<p data-i18n='toolbar.authenticate'></p>
<ul>
<li id='profile_auth_identity'></li>
<li id='profile_button_login'>
<a class='authButton' data-i18n='toolbar.login'></a>
</li>
<li id='profile_button_logout'>
<a class='authButton' data-i18n='toolbar.logout'></a>
</li>
</ul>
</div>
</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}`));
}
export default {
init(emitter) {
initHTML();
const settings = APP.store.getState()['features/base/settings'];
/**
* Updates display name.
*
* @returns {void}
*/
function updateDisplayName() {
emitter.emit(UIEvents.NICKNAME_CHANGED, $('#setDisplayName').val());
}
$('#setDisplayName')
.val(settings.displayName)
.keyup(event => {
if (event.keyCode === 13) { // enter
updateDisplayName();
}
})
.focusout(updateDisplayName);
/**
* Updates the email.
*
* @returns {void}
*/
function updateEmail() {
emitter.emit(UIEvents.EMAIL_CHANGED, $('#setEmail').val());
}
$('#setEmail')
.val(settings.email)
.keyup(event => {
if (event.keyCode === 13) { // enter
updateEmail();
}
})
.focusout(updateEmail);
/**
*
*/
function loginClicked() {
sendAnalytics(createProfilePanelButtonEvent('login.button'));
emitter.emit(UIEvents.AUTH_CLICKED);
}
$('#profile_button_login').click(loginClicked);
/**
*
*/
function logoutClicked() {
const titleKey = 'dialog.logoutTitle';
const msgKey = 'dialog.logoutQuestion';
sendAnalytics(createProfilePanelButtonEvent('logout.button'));
// Ask for confirmation
APP.UI.messageHandler.openTwoButtonDialog({
titleKey,
msgKey,
leftButtonKey: 'dialog.Yes',
submitFunction(evt, yes) {
if (yes) {
emitter.emit(UIEvents.LOGOUT);
}
}
});
}
$('#profile_button_logout').click(logoutClicked);
},
/**
* Check if settings menu is visible or not.
* @returns {boolean}
*/
isVisible() {
return UIUtil.isVisible(document.getElementById('profile_container'));
},
/**
* Change user display name in the settings menu.
* @param {string} newDisplayName
*/
changeDisplayName(newDisplayName) {
$('#setDisplayName').val(newDisplayName);
},
/**
* Change the value of the field for the user email.
* @param {string} email the new value that will be displayed in the field.
*/
changeEmail(email) {
$('#setEmail').val(email);
},
/**
* Shows or hides authentication related buttons
* @param {boolean} show <tt>true</tt> to show or <tt>false</tt> to hide
*/
showAuthenticationButtons(show) {
const id = 'profile_auth_container';
UIUtil.setVisible(id, show);
},
/**
* Shows/hides login button.
* @param {boolean} show <tt>true</tt> to show or <tt>false</tt> to hide
*/
showLoginButton(show) {
const id = 'profile_button_login';
UIUtil.setVisible(id, show);
},
/**
* Shows/hides logout button.
* @param {boolean} show <tt>true</tt> to show or <tt>false</tt> to hide
*/
showLogoutButton(show) {
const id = 'profile_button_logout';
UIUtil.setVisible(id, show);
},
/**
* Displays user's authenticated identity name (login).
* @param {string} authIdentity identity name to be displayed.
*/
setAuthenticatedIdentity(authIdentity) {
const id = 'profile_auth_identity';
UIUtil.setVisible(id, Boolean(authIdentity));
$(`#${id}`).text(authIdentity ? authIdentity : '');
}
};

View File

@ -1,52 +0,0 @@
/* 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,
isSettingEnabled
} from '../../../../react/features/settings';
import UIUtil from '../../util/UIUtil';
/* eslint-enable no-unused-vars */
export default {
init() {
const settingsMenuContainer = document.createElement('div');
settingsMenuContainer.id = 'settings_container';
settingsMenuContainer.className = 'sideToolbarContainer__inner';
$('#sideToolbarContainer').append(settingsMenuContainer);
const props = {
showDeviceSettings: isSettingEnabled('devices'),
showLanguageSettings: isSettingEnabled('language'),
showModeratorSettings: isSettingEnabled('moderator'),
showTitles: interfaceConfig.SETTINGS_SECTIONS.length > 1
};
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<SettingsMenu { ...props } />
</I18nextProvider>
</Provider>,
settingsMenuContainer
);
},
/**
* Check if settings menu is visible or not.
* @returns {boolean}
*/
isVisible() {
return UIUtil.isVisible(document.getElementById('settings_container'));
}
};

88
package-lock.json generated
View File

@ -37,6 +37,94 @@
"styled-components": "^1.3.0" "styled-components": "^1.3.0"
} }
}, },
"@atlaskit/checkbox": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@atlaskit/checkbox/-/checkbox-2.0.2.tgz",
"integrity": "sha512-sX5vixywY61A7Q/YR04g3Z+PhaZNEQ1orc/t2JX80iF+FLpEfvD1KY0ywraSHx5ljXjxJ9u6Df4az/y9BDmQ5A==",
"requires": {
"@atlaskit/button": "7.2.5",
"@atlaskit/icon": "11.3.0",
"@atlaskit/theme": "3.2.2",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.4.6"
},
"dependencies": {
"@atlaskit/analytics-next": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@atlaskit/analytics-next/-/analytics-next-2.1.2.tgz",
"integrity": "sha512-rAd+kLEEXZ8mu39qOf+E0kZu+k1RWNwd5vyfb0WXuBxq+jyCzAg19vhj99Uq2TblW/WgOX9ajBE+lEgbYqmvNw==",
"requires": {
"prop-types": "15.6.0"
}
},
"@atlaskit/button": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/@atlaskit/button/-/button-7.2.5.tgz",
"integrity": "sha512-YDH2wWxoMe9uGmyMy+zPQMbxkK0TrFplutu6bZ0n8Ojet8XcGOBPyWS5lf3Nt+DOPKLUXCI4pknR6fB6ZF1e/g==",
"requires": {
"@atlaskit/analytics-next": "2.1.2",
"@atlaskit/spinner": "5.0.2",
"@atlaskit/theme": "3.2.2",
"babel-runtime": "6.26.0",
"styled-components": "1.4.6"
}
},
"@atlaskit/icon": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/@atlaskit/icon/-/icon-11.3.0.tgz",
"integrity": "sha512-dFnpk3yT9EZUmCC8bUOP4WmENWMqLYezBOpv+mp/vKBbzT786c+ZVyDW5wZ9hSKmfb+aHjiZt+UuwUiVW5D+Wg==",
"requires": {
"@atlaskit/theme": "3.2.2",
"babel-runtime": "6.26.0",
"styled-components": "1.4.6",
"uuid": "3.1.0"
}
},
"@atlaskit/spinner": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@atlaskit/spinner/-/spinner-5.0.2.tgz",
"integrity": "sha512-n0j/urjG3FF9q/6Nae981GwsdvT44zAobPqFGnaeKDfqUzrFHcs1PmL0dqa36aFJzOPZHzl6ZfBl9Q3Vpl9PKQ==",
"requires": {
"@atlaskit/theme": "3.2.2",
"babel-runtime": "6.26.0",
"react-transition-group": "2.3.1",
"styled-components": "1.4.6"
}
},
"@atlaskit/theme": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-3.2.2.tgz",
"integrity": "sha512-SQYgGe8WnO1aF991XraCzbMzOf8v1rMBkLYkwVb6BAjxTVgeSepMTVYOTQ7+KXzSAKFP0fDxgXnUB/VsSUR8Ig==",
"requires": {
"prop-types": "15.6.0",
"styled-components": "1.4.6"
}
},
"react-transition-group": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.3.1.tgz",
"integrity": "sha512-hu4/LAOFSKjWt1+1hgnOv3ldxmt6lvZGTWz4KUkFrqzXrNDIVSu6txIcPszw7PNduR8en9YTN55JLRyd/L1ZiQ==",
"requires": {
"dom-helpers": "3.3.1",
"loose-envify": "1.3.1",
"prop-types": "15.6.1"
},
"dependencies": {
"prop-types": {
"version": "15.6.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz",
"integrity": "sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ==",
"requires": {
"fbjs": "0.8.16",
"loose-envify": "1.3.1",
"object-assign": "4.1.1"
}
}
}
}
}
},
"@atlaskit/dropdown-menu": { "@atlaskit/dropdown-menu": {
"version": "3.10.2", "version": "3.10.2",
"resolved": "https://registry.npmjs.org/@atlaskit/dropdown-menu/-/dropdown-menu-3.10.2.tgz", "resolved": "https://registry.npmjs.org/@atlaskit/dropdown-menu/-/dropdown-menu-3.10.2.tgz",

View File

@ -17,6 +17,7 @@
"dependencies": { "dependencies": {
"@atlaskit/avatar": "8.0.5", "@atlaskit/avatar": "8.0.5",
"@atlaskit/button": "5.4.2", "@atlaskit/button": "5.4.2",
"@atlaskit/checkbox": "2.0.2",
"@atlaskit/dropdown-menu": "3.10.2", "@atlaskit/dropdown-menu": "3.10.2",
"@atlaskit/droplist": "4.11.1", "@atlaskit/droplist": "4.11.1",
"@atlaskit/field-text": "4.0.1", "@atlaskit/field-text": "4.0.1",

View File

@ -1,3 +1,15 @@
/**
* The type of (redux) action which signals that server authentication has
* becoming available or unavailable or logged in user has changed.
*
* {
* type: AUTH_STATUS_CHANGED,
* authEnabled: boolean,
* authLogin: string
* }
*/
export const AUTH_STATUS_CHANGED = Symbol('AUTH_STATUS_CHANGED');
/** /**
* The type of (redux) action which signals that a specific conference failed. * The type of (redux) action which signals that a specific conference failed.
* *

View File

@ -22,6 +22,7 @@ import { getLocalTracks, trackAdded, trackRemoved } from '../tracks';
import { getJitsiMeetGlobalNS } from '../util'; import { getJitsiMeetGlobalNS } from '../util';
import { import {
AUTH_STATUS_CHANGED,
CONFERENCE_FAILED, CONFERENCE_FAILED,
CONFERENCE_JOINED, CONFERENCE_JOINED,
CONFERENCE_LEFT, CONFERENCE_LEFT,
@ -178,6 +179,26 @@ function _addConferenceListeners(conference, dispatch) {
}))); })));
} }
/**
* Updates the current known state of server-side authentication.
*
* @param {boolean} authEnabled - Whether or not server authentication is
* enabled.
* @param {string} authLogin - The current name of the logged in user, if any.
* @returns {{
* type: AUTH_STATUS_CHANGED,
* authEnabled: boolean,
* authLogin: string
* }}
*/
export function authStatusChanged(authEnabled: boolean, authLogin: string) {
return {
type: AUTH_STATUS_CHANGED,
authEnabled,
authLogin
};
}
/** /**
* Signals that a specific conference has failed. * Signals that a specific conference has failed.
* *

View File

@ -6,6 +6,7 @@ import { assign, ReducerRegistry, set } from '../redux';
import { LOCKED_LOCALLY, LOCKED_REMOTELY } from '../../room-lock'; import { LOCKED_LOCALLY, LOCKED_REMOTELY } from '../../room-lock';
import { import {
AUTH_STATUS_CHANGED,
CONFERENCE_FAILED, CONFERENCE_FAILED,
CONFERENCE_JOINED, CONFERENCE_JOINED,
CONFERENCE_LEFT, CONFERENCE_LEFT,
@ -31,6 +32,9 @@ import { isRoomValid } from './functions';
*/ */
ReducerRegistry.register('features/base/conference', (state = {}, action) => { ReducerRegistry.register('features/base/conference', (state = {}, action) => {
switch (action.type) { switch (action.type) {
case AUTH_STATUS_CHANGED:
return _authStatusChanged(state, action);
case CONFERENCE_FAILED: case CONFERENCE_FAILED:
return _conferenceFailed(state, action); return _conferenceFailed(state, action);
@ -85,6 +89,23 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
return state; return state;
}); });
/**
* Reduces a specific Redux action AUTH_STATUS_CHANGED of the feature
* base/conference.
*
* @param {Object} state - The Redux state of the feature base/conference.
* @param {Action} action - The Redux action AUTH_STATUS_CHANGED to reduce.
* @private
* @returns {Object} The new state of the feature base/conference after the
* reduction of the specified action.
*/
function _authStatusChanged(state, { authEnabled, authLogin }) {
return assign(state, {
authEnabled,
authLogin
});
}
/** /**
* Reduces a specific Redux action CONFERENCE_FAILED of the feature * Reduces a specific Redux action CONFERENCE_FAILED of the feature
* base/conference. * base/conference.

View File

@ -0,0 +1,67 @@
// @flow
import { Component } from 'react';
/**
* The type of the React {@code Component} props of {@link AbstractDialogTab}.
*/
export type Props = {
/**
* Function that closes the dialog.
*/
closeDialog: Function,
/**
* Callback to invoke on change.
*/
onTabStateChange: Function,
/**
* The id of the tab.
*/
tabId: number
};
/**
* Abstract React {@code Component} for tabs of the DialogWithTabs component.
*
* @extends Component
*/
class AbstractDialogTab extends Component<Props> {
/**
* Initializes a new {@code AbstractDialogTab} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onChange = this._onChange.bind(this);
}
_onChange: (Object) => {};
/**
* Uses the onTabStateChange function to pass the changed state of the
* controlled tab component to the controlling DialogWithTabs component.
*
* @param {Object} change - Object that contains the changed property and
* value.
* @returns {void}
*/
_onChange(change) {
const { onTabStateChange, tabId } = this.props;
onTabStateChange(tabId, {
...this.props,
...change
});
}
}
export default AbstractDialogTab;

View File

@ -0,0 +1,199 @@
// @flow
import Tabs from '@atlaskit/tabs';
import React, { Component } from 'react';
import { StatelessDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* The type of the React {@code Component} props of {@link DialogWithTabs}.
*/
export type Props = {
/**
* Function that closes the dialog.
*/
closeDialog: Function,
/**
* Which settings tab should be initially displayed. If not defined then
* the first tab will be displayed.
*/
defaultTab: number,
/**
* Disables dismissing the dialog when the blanket is clicked. Enabled
* by default.
*/
disableBlanketClickDismiss: boolean,
/**
* Callback invoked when the Save button has been pressed.
*/
onSubmit: Function,
/**
* Invoked to obtain translated strings.
*/
t: Function,
/**
* Information about the tabs that will be rendered.
*/
tabs: Array<Object>
};
/**
* The type of the React {@code Component} state of {@link DialogWithTabs}.
*/
type State = {
/**
* An array of the states of the tabs.
*/
tabStates: Array<Object>
};
/**
* A React {@code Component} for displaying a dialog with tabs.
*
* @extends Component
*/
class DialogWithTabs extends Component<Props, State> {
/**
* Initializes a new {@code DialogWithTabs} instance.
*
* @param {Object} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.state = {
tabStates: this.props.tabs.map(tab => tab.props)
};
this._onSubmit = this._onSubmit.bind(this);
this._onTabStateChange = this._onTabStateChange.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const onCancel = this.props.closeDialog;
return (
<StatelessDialog
disableBlanketClickDismiss
= { this.props.disableBlanketClickDismiss }
onCancel = { onCancel }
onSubmit = { this._onSubmit }
titleKey = 'settings.title'>
<div className = 'settings-dialog'>
{ this._renderTabs() }
</div>
</StatelessDialog>
);
}
/**
* Renders the tabs from the tab information passed on props.
*
* @returns {void}
*/
_renderTabs() {
const { defaultTab = 0, t, tabs } = this.props;
if (tabs.length === 1) {
return this._renderTab({
...tabs[0],
tabId: 0
});
}
if (tabs.length > 1) {
return (
<Tabs
tabs = {
tabs.map(({ component, label, styles }, idx) => {
return {
content: this._renderTab({
component,
styles,
tabId: idx
}),
defaultSelected: defaultTab === idx,
label: t(label)
};
})
} />);
}
logger.warn('No settings tabs configured to display.');
return null;
}
/**
* Renders a tab from the tab information passed as parameters.
*
* @param {Object} tabInfo - Information about the tab.
* @returns {Component} - The tab.
*/
_renderTab({ component, styles, tabId }) {
const { closeDialog } = this.props;
const TabComponent = component;
return (
<div className = { styles }>
<TabComponent
closeDialog = { closeDialog }
onTabStateChange
= { this._onTabStateChange }
tabId = { tabId }
{ ...this.state.tabStates[tabId] } />
</div>);
}
_onTabStateChange: (number, Object) => void;
/**
* Changes the state for a tab.
*
* @param {number} tabId - The id of the tab which state will be changed.
* @param {Object} state - The new state.
* @returns {void}
*/
_onTabStateChange(tabId, state) {
const tabStates = [ ...this.state.tabStates ];
tabStates[tabId] = state;
this.setState({ tabStates });
}
_onSubmit: () => void;
/**
* Submits the information filled in the dialog.
*
* @returns {void}
*/
_onSubmit() {
const { onSubmit, tabs } = this.props;
tabs.forEach(({ submit }, idx) => {
submit(this.state.tabStates[idx]);
});
onSubmit();
}
}
export default translate(DialogWithTabs);

View File

@ -1,4 +1,9 @@
// @flow
export { default as BottomSheet } from './BottomSheet'; export { default as BottomSheet } from './BottomSheet';
export { default as DialogContainer } from './DialogContainer'; export { default as DialogContainer } from './DialogContainer';
export { default as Dialog } from './Dialog'; export { default as Dialog } from './Dialog';
export { default as StatelessDialog } from './StatelessDialog'; export { default as StatelessDialog } from './StatelessDialog';
export { default as DialogWithTabs } from './DialogWithTabs';
export { default as AbstractDialogTab } from './AbstractDialogTab';
export type { Props as AbstractDialogTabProps } from './AbstractDialogTab';

View File

@ -1,5 +1,3 @@
/* globals APP, interfaceConfig */
import { API_ID } from '../../../modules/API/constants'; import { API_ID } from '../../../modules/API/constants';
import { import {
PostMessageTransportBackend, PostMessageTransportBackend,
@ -12,66 +10,18 @@ import {
setAudioOutputDevice, setAudioOutputDevice,
setVideoInputDevice setVideoInputDevice
} from '../base/devices'; } from '../base/devices';
import { openDialog } from '../base/dialog';
import { i18next } from '../base/i18n'; import { i18next } from '../base/i18n';
import JitsiMeetJS from '../base/lib-jitsi-meet'; import JitsiMeetJS from '../base/lib-jitsi-meet';
import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes'; import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
import { DeviceSelectionDialog } from './components'; import { getDeviceSelectionDialogProps } from './functions';
/**
* Open DeviceSelectionDialog with a configuration based on the environment's
* supported abilities.
*
* @returns {Function}
*/
export function openDeviceSelectionDialog() {
return dispatch => {
if (interfaceConfig.filmStripOnly) {
dispatch(_openDeviceSelectionDialogInPopup());
} else {
dispatch(_openDeviceSelectionDialogHere());
}
};
}
/**
* Opens the DeviceSelectionDialog in the same window.
*
* @returns {Function}
*/
function _openDeviceSelectionDialogHere() {
return dispatch =>
JitsiMeetJS.mediaDevices.isDeviceListAvailable()
.then(isDeviceListAvailable => {
const settings = APP.store.getState()['features/base/settings'];
dispatch(openDialog(DeviceSelectionDialog, {
currentAudioInputId: settings.micDeviceId,
currentAudioOutputId: getAudioOutputDeviceId(),
currentVideoInputId: settings.cameraDeviceId,
disableAudioInputChange:
!JitsiMeetJS.isMultipleAudioInputSupported(),
disableDeviceChange: !isDeviceListAvailable
|| !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
hasAudioPermission: JitsiMeetJS.mediaDevices
.isDevicePermissionGranted.bind(null, 'audio'),
hasVideoPermission: JitsiMeetJS.mediaDevices
.isDevicePermissionGranted.bind(null, 'video'),
hideAudioInputPreview:
!JitsiMeetJS.isCollectingLocalStats(),
hideAudioOutputSelect: !JitsiMeetJS.mediaDevices
.isDeviceChangeAvailable('output')
}));
});
}
/** /**
* Opens a popup window with the device selection dialog in it. * Opens a popup window with the device selection dialog in it.
* *
* @returns {Function} * @returns {Function}
*/ */
function _openDeviceSelectionDialogInPopup() { export function openDeviceSelectionPopup() {
return (dispatch, getState) => { return (dispatch, getState) => {
const { popupDialogData } = getState()['features/device-selection']; const { popupDialogData } = getState()['features/device-selection'];
@ -218,3 +168,36 @@ function _setDeviceSelectionPopupData(popupDialogData) {
popupDialogData popupDialogData
}; };
} }
/**
* Submits the settings related to device selection.
*
* @param {Object} newState - The new settings.
* @returns {Function}
*/
export function submitDeviceSelectionTab(newState) {
return (dispatch, getState) => {
const currentState = getDeviceSelectionDialogProps(getState());
if (newState.selectedVideoInputId
&& newState.selectedVideoInputId
!== currentState.selectedVideoInputId) {
dispatch(
setVideoInputDevice(newState.selectedVideoInputId));
}
if (newState.selectedAudioInputId
&& newState.selectedAudioInputId
!== currentState.selectedAudioInputId) {
dispatch(
setAudioInputDevice(newState.selectedAudioInputId));
}
if (newState.selectedAudioOutputId
&& newState.selectedAudioOutputId
!== currentState.selectedAudioOutputId) {
dispatch(
setAudioOutputDevice(newState.selectedAudioOutputId));
}
};
}

View File

@ -0,0 +1,354 @@
// @flow
import React from 'react';
import { AbstractDialogTab } from '../../base/dialog';
import type { Props as AbstractDialogTabProps } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { createLocalTrack } from '../../base/lib-jitsi-meet';
import AudioInputPreview from './AudioInputPreview';
import AudioOutputPreview from './AudioOutputPreview';
import DeviceSelector from './DeviceSelector';
import VideoInputPreview from './VideoInputPreview';
/**
* The type of the React {@code Component} props of {@link DeviceSelection}.
*/
export type Props = {
...$Exact<AbstractDialogTabProps>,
/**
* All known audio and video devices split by type. This prop comes from
* the app state.
*/
availableDevices: Object,
/**
* Whether or not the audio selector can be interacted with. If true,
* the audio input selector will be rendered as disabled. This is
* specifically used to prevent audio device changing in Firefox, which
* currently does not work due to a browser-side regression.
*/
disableAudioInputChange: boolean,
/**
* True if device changing is configured to be disallowed. Selectors
* will display as disabled.
*/
disableDeviceChange: boolean,
/**
* Function that checks whether or not a new audio input source can be
* selected.
*/
hasAudioPermission: Function,
/**
* Function that checks whether or not a new video input sources can be
* selected.
*/
hasVideoPermission: Function,
/**
* If true, the audio meter will not display. Necessary for browsers or
* configurations that do not support local stats to prevent a
* non-responsive mic preview from displaying.
*/
hideAudioInputPreview: boolean,
/**
* Whether or not the audio output source selector should display. If
* true, the audio output selector and test audio link will not be
* rendered. This is specifically used for hiding audio output on
* temasys browsers which do not support such change.
*/
hideAudioOutputSelect: boolean,
/**
* The id of the audio input device to preview.
*/
selectedAudioInputId: string,
/**
* The id of the audio output device to preview.
*/
selectedAudioOutputId: string,
/**
* The id of the video input device to preview.
*/
selectedVideoInputId: string,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* The type of the React {@code Component} state of {@link DeviceSelection}.
*/
type State = {
/**
* The JitsiTrack to use for previewing audio input.
*/
previewAudioTrack: ?Object,
/**
* The JitsiTrack to use for previewing video input.
*/
previewVideoTrack: ?Object,
/**
* The error message from trying to use a video input device.
*/
previewVideoTrackError: ?string
};
/**
* React {@code Component} for previewing audio and video input/output devices.
*
* @extends Component
*/
class DeviceSelection extends AbstractDialogTab<Props, State> {
/**
* Initializes a new DeviceSelection instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.state = {
previewAudioTrack: null,
previewVideoTrack: null,
previewVideoTrackError: null
};
}
/**
* Generate the initial previews for audio input and video input.
*
* @inheritdoc
*/
componentDidMount() {
this._createAudioInputTrack(this.props.selectedAudioInputId);
this._createVideoInputTrack(this.props.selectedVideoInputId);
}
/**
* Updates audio input and video input previews.
*
* @inheritdoc
* @param {Object} nextProps - The read-only props which this Component will
* receive.
* @returns {void}
*/
componentWillReceiveProps(nextProps: Object) {
const { selectedAudioInputId, selectedVideoInputId } = this.props;
if (selectedAudioInputId !== nextProps.selectedAudioInputId) {
this._createAudioInputTrack(nextProps.selectedAudioInputId);
}
if (selectedVideoInputId !== nextProps.selectedVideoInputId) {
this._createVideoInputTrack(nextProps.selectedVideoInputId);
}
}
/**
* Ensure preview tracks are destroyed to prevent continued use.
*
* @inheritdoc
*/
componentWillUnmount() {
this._disposeAudioInputPreview();
this._disposeVideoInputPreview();
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
const {
hideAudioInputPreview,
hideAudioOutputSelect,
selectedAudioOutputId
} = this.props;
return (
<div className = 'device-selection'>
<div className = 'device-selection-column column-video'>
<div className = 'device-selection-video-container'>
<VideoInputPreview
error = { this.state.previewVideoTrackError }
track = { this.state.previewVideoTrack } />
</div>
{ !hideAudioInputPreview
&& <AudioInputPreview
track = { this.state.previewAudioTrack } /> }
</div>
<div className = 'device-selection-column column-selectors'>
<div className = 'device-selectors'>
{ this._renderSelectors() }
</div>
{ !hideAudioOutputSelect
&& <AudioOutputPreview
deviceId = { selectedAudioOutputId } /> }
</div>
</div>
);
}
/**
* Creates the JitiTrack for the audio input preview.
*
* @param {string} deviceId - The id of audio input device to preview.
* @private
* @returns {void}
*/
_createAudioInputTrack(deviceId) {
this._disposeAudioInputPreview()
.then(() => createLocalTrack('audio', deviceId))
.then(jitsiLocalTrack => {
this.setState({
previewAudioTrack: jitsiLocalTrack
});
})
.catch(() => {
this.setState({
previewAudioTrack: null
});
});
}
/**
* Creates the JitiTrack for the video input preview.
*
* @param {string} deviceId - The id of video device to preview.
* @private
* @returns {void}
*/
_createVideoInputTrack(deviceId) {
this._disposeVideoInputPreview()
.then(() => createLocalTrack('video', deviceId))
.then(jitsiLocalTrack => {
if (!jitsiLocalTrack) {
return Promise.reject();
}
this.setState({
previewVideoTrack: jitsiLocalTrack,
previewVideoTrackError: null
});
})
.catch(() => {
this.setState({
previewVideoTrack: null,
previewVideoTrackError:
this.props.t('deviceSelection.previewUnavailable')
});
});
}
/**
* Utility function for disposing the current audio input preview.
*
* @private
* @returns {Promise}
*/
_disposeAudioInputPreview(): Promise<*> {
return this.state.previewAudioTrack
? this.state.previewAudioTrack.dispose() : Promise.resolve();
}
/**
* Utility function for disposing the current video input preview.
*
* @private
* @returns {Promise}
*/
_disposeVideoInputPreview(): Promise<*> {
return this.state.previewVideoTrack
? this.state.previewVideoTrack.dispose() : Promise.resolve();
}
/**
* Creates a DeviceSelector instance based on the passed in configuration.
*
* @private
* @param {Object} deviceSelectorProps - The props for the DeviceSelector.
* @returns {ReactElement}
*/
_renderSelector(deviceSelectorProps) {
return (
<div key = { deviceSelectorProps.label }>
<div className = 'device-selector-label'>
{ this.props.t(deviceSelectorProps.label) }
</div>
<DeviceSelector { ...deviceSelectorProps } />
</div>
);
}
/**
* Creates DeviceSelector instances for video output, audio input, and audio
* output.
*
* @private
* @returns {Array<ReactElement>} DeviceSelector instances.
*/
_renderSelectors() {
const { availableDevices } = this.props;
const configurations = [
{
devices: availableDevices.videoInput,
hasPermission: this.props.hasVideoPermission(),
icon: 'icon-camera',
isDisabled: this.props.disableDeviceChange,
key: 'videoInput',
label: 'settings.selectCamera',
onSelect: selectedVideoInputId =>
super._onChange({ selectedVideoInputId }),
selectedDeviceId: this.props.selectedVideoInputId
},
{
devices: availableDevices.audioInput,
hasPermission: this.props.hasAudioPermission(),
icon: 'icon-microphone',
isDisabled: this.props.disableAudioInputChange
|| this.props.disableDeviceChange,
key: 'audioInput',
label: 'settings.selectMic',
onSelect: selectedAudioInputId =>
super._onChange({ selectedAudioInputId }),
selectedDeviceId: this.props.selectedAudioInputId
}
];
if (!this.props.hideAudioOutputSelect) {
configurations.push({
devices: availableDevices.audioOutput,
hasPermission: this.props.hasAudioPermission()
|| this.props.hasVideoPermission(),
icon: 'icon-volume',
isDisabled: this.props.disableDeviceChange,
key: 'audioOutput',
label: 'settings.selectAudioOutput',
onSelect: selectedAudioOutputId =>
super._onChange({ selectedAudioOutputId }),
selectedDeviceId: this.props.selectedAudioOutputId
});
}
return configurations.map(config => this._renderSelector(config));
}
}
export default translate(DeviceSelection);

View File

@ -1,165 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
setAudioInputDevice,
setAudioOutputDevice,
setVideoInputDevice
} from '../../base/devices';
import { hideDialog } from '../../base/dialog';
import DeviceSelectionDialogBase from './DeviceSelectionDialogBase';
/**
* React component for previewing and selecting new audio and video sources.
*
* @extends Component
*/
class DeviceSelectionDialog extends Component {
/**
* DeviceSelectionDialog component's property types.
*
* @static
*/
static propTypes = {
/**
* All known audio and video devices split by type. This prop comes from
* the app state.
*/
_availableDevices: PropTypes.object,
/**
* Device id for the current audio input device. This device will be set
* as the default audio input device to preview.
*/
currentAudioInputId: PropTypes.string,
/**
* Device id for the current audio output device. This device will be
* set as the default audio output device to preview.
*/
currentAudioOutputId: PropTypes.string,
/**
* Device id for the current video input device. This device will be set
* as the default video input device to preview.
*/
currentVideoInputId: PropTypes.string,
/**
* Whether or not the audio selector can be interacted with. If true,
* the audio input selector will be rendered as disabled. This is
* specifically used to prevent audio device changing in Firefox, which
* currently does not work due to a browser-side regression.
*/
disableAudioInputChange: PropTypes.bool,
/**
* True if device changing is configured to be disallowed. Selectors
* will display as disabled.
*/
disableDeviceChange: PropTypes.bool,
/**
* Invoked to notify the store of app state changes.
*/
dispatch: PropTypes.func,
/**
* Function that checks whether or not a new audio input source can be
* selected.
*/
hasAudioPermission: PropTypes.func,
/**
* Function that checks whether or not a new video input sources can be
* selected.
*/
hasVideoPermission: PropTypes.func,
/**
* If true, the audio meter will not display. Necessary for browsers or
* configurations that do not support local stats to prevent a
* non-responsive mic preview from displaying.
*/
hideAudioInputPreview: PropTypes.bool,
/**
* Whether or not the audio output source selector should display. If
* true, the audio output selector and test audio link will not be
* rendered. This is specifically used for hiding audio output on
* temasys browsers which do not support such change.
*/
hideAudioOutputSelect: PropTypes.bool
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
const {
currentAudioInputId,
currentAudioOutputId,
currentVideoInputId,
disableAudioInputChange,
disableDeviceChange,
dispatch,
hasAudioPermission,
hasVideoPermission,
hideAudioInputPreview,
hideAudioOutputSelect
} = this.props;
const props = {
availableDevices: this.props._availableDevices,
closeModal: () => dispatch(hideDialog()),
currentAudioInputId,
currentAudioOutputId,
currentVideoInputId,
disableAudioInputChange,
disableDeviceChange,
hasAudioPermission,
hasVideoPermission,
hideAudioInputPreview,
hideAudioOutputSelect,
setAudioInputDevice: id => {
dispatch(setAudioInputDevice(id));
return Promise.resolve();
},
setAudioOutputDevice: id => {
dispatch(setAudioOutputDevice(id));
return Promise.resolve();
},
setVideoInputDevice: id => {
dispatch(setVideoInputDevice(id));
return Promise.resolve();
}
};
return <DeviceSelectionDialogBase { ...props } />;
}
}
/**
* Maps (parts of) the Redux state to the associated DeviceSelectionDialog's
* props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _availableDevices: Object
* }}
*/
function _mapStateToProps(state) {
return {
_availableDevices: state['features/base/devices']
};
}
export default connect(_mapStateToProps)(DeviceSelectionDialog);

View File

@ -1,560 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { StatelessDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { createLocalTrack } from '../../base/lib-jitsi-meet';
import { shouldShowOnlyDeviceSelection } from '../../settings';
import AudioInputPreview from './AudioInputPreview';
import AudioOutputPreview from './AudioOutputPreview';
import DeviceSelector from './DeviceSelector';
import VideoInputPreview from './VideoInputPreview';
/**
* React component for previewing and selecting new audio and video sources.
*
* @extends Component
*/
class DeviceSelectionDialogBase extends Component {
/**
* DeviceSelectionDialogBase component's property types.
*
* @static
*/
static propTypes = {
/**
* All known audio and video devices split by type. This prop comes from
* the app state.
*/
availableDevices: PropTypes.object,
/**
* Closes the dialog.
*/
closeModal: PropTypes.func,
/**
* Device id for the current audio input device. This device will be set
* as the default audio input device to preview.
*/
currentAudioInputId: PropTypes.string,
/**
* Device id for the current audio output device. This device will be
* set as the default audio output device to preview.
*/
currentAudioOutputId: PropTypes.string,
/**
* Device id for the current video input device. This device will be set
* as the default video input device to preview.
*/
currentVideoInputId: PropTypes.string,
/**
* Whether or not the audio selector can be interacted with. If true,
* the audio input selector will be rendered as disabled. This is
* specifically used to prevent audio device changing in Firefox, which
* currently does not work due to a browser-side regression.
*/
disableAudioInputChange: PropTypes.bool,
/**
* Disables dismissing the dialog when the blanket is clicked. Enabled
* by default.
*/
disableBlanketClickDismiss: PropTypes.bool,
/**
* True if device changing is configured to be disallowed. Selectors
* will display as disabled.
*/
disableDeviceChange: PropTypes.bool,
/**
* Function that checks whether or not a new audio input source can be
* selected.
*/
hasAudioPermission: PropTypes.func,
/**
* Function that checks whether or not a new video input sources can be
* selected.
*/
hasVideoPermission: PropTypes.func,
/**
* If true, the audio meter will not display. Necessary for browsers or
* configurations that do not support local stats to prevent a
* non-responsive mic preview from displaying.
*/
hideAudioInputPreview: PropTypes.bool,
/**
* Whether or not the audio output source selector should display. If
* true, the audio output selector and test audio link will not be
* rendered. This is specifically used for hiding audio output on
* temasys browsers which do not support such change.
*/
hideAudioOutputSelect: PropTypes.bool,
/**
* Function that sets the audio input device.
*/
setAudioInputDevice: PropTypes.func,
/**
* Function that sets the audio output device.
*/
setAudioOutputDevice: PropTypes.func,
/**
* Function that sets the video input device.
*/
setVideoInputDevice: PropTypes.func,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
/**
* Initializes a new DeviceSelectionDialogBase instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props) {
super(props);
const { availableDevices } = this.props;
this.state = {
// JitsiLocalTrack to use for live previewing of audio input.
previewAudioTrack: null,
// JitsiLocalTrack to use for live previewing of video input.
previewVideoTrack: null,
// An message describing a problem with obtaining a video preview.
previewVideoTrackError: null,
// The audio input device id to show as selected by default.
selectedAudioInputId: this.props.currentAudioInputId || '',
// The audio output device id to show as selected by default.
selectedAudioOutputId: this.props.currentAudioOutputId || '',
// The video input device id to show as selected by default.
// FIXME: On temasys, without a device selected and put into local
// storage as the default device to use, the current video device id
// is a blank string. This is because the library gets a local video
// track and then maps the track's device id by matching the track's
// label to the MediaDeviceInfos returned from enumerateDevices. In
// WebRTC, the track label is expected to return the camera device
// label. However, temasys video track labels refer to track id, not
// device label, so the library cannot match the track to a device.
// The workaround of defaulting to the first videoInput available
// is re-used from the previous device settings implementation.
selectedVideoInputId: this.props.currentVideoInputId
|| (availableDevices.videoInput
&& availableDevices.videoInput[0]
&& availableDevices.videoInput[0].deviceId)
|| ''
};
// Preventing closing while cleaning up previews is important for
// supporting temasys video cleanup. Temasys requires its video object
// to be in the dom and visible for proper detaching of tracks. Delaying
// closure until cleanup is complete ensures no errors in the process.
this._isClosing = false;
this._setDevicesAndClose = this._setDevicesAndClose.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onSubmit = this._onSubmit.bind(this);
this._updateAudioOutput = this._updateAudioOutput.bind(this);
this._updateAudioInput = this._updateAudioInput.bind(this);
this._updateVideoInput = this._updateVideoInput.bind(this);
}
/**
* Sets default device choices so a choice is pre-selected in the dropdowns
* and live previews are created.
*
* @inheritdoc
*/
componentDidMount() {
this._updateAudioOutput(this.state.selectedAudioOutputId);
this._updateAudioInput(this.state.selectedAudioInputId);
this._updateVideoInput(this.state.selectedVideoInputId);
}
/**
* Disposes preview tracks that might not already be disposed.
*
* @inheritdoc
*/
componentWillUnmount() {
// This handles the case where neither submit nor cancel were triggered,
// such as on modal switch. In that case, make a dying attempt to clean
// up previews.
if (!this._isClosing) {
this._attemptPreviewTrackCleanup();
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
return (
<StatelessDialog
cancelTitleKey = { 'dialog.Cancel' }
disableBlanketClickDismiss
= { this.props.disableBlanketClickDismiss }
okTitleKey = { 'dialog.Save' }
onCancel = { this._onCancel }
onSubmit = { this._onSubmit }
titleKey = { this._getModalTitle() }>
<div className = 'device-selection'>
<div className = 'device-selection-column column-video'>
<div className = 'device-selection-video-container'>
<VideoInputPreview
error = { this.state.previewVideoTrackError }
track = { this.state.previewVideoTrack } />
</div>
{ this._renderAudioInputPreview() }
</div>
<div className = 'device-selection-column column-selectors'>
<div className = 'device-selectors'>
{ this._renderSelectors() }
</div>
{ this._renderAudioOutputPreview() }
</div>
</div>
</StatelessDialog>
);
}
/**
* Cleans up preview tracks if they are not active tracks.
*
* @private
* @returns {Array<Promise>} Zero to two promises will be returned. One
* promise can be for video cleanup and another for audio cleanup.
*/
_attemptPreviewTrackCleanup() {
return Promise.all([
this._disposeVideoPreview(),
this._disposeAudioPreview()
]);
}
/**
* Utility function for disposing the current audio preview.
*
* @private
* @returns {Promise}
*/
_disposeAudioPreview() {
return this.state.previewAudioTrack
? this.state.previewAudioTrack.dispose() : Promise.resolve();
}
/**
* Utility function for disposing the current video preview.
*
* @private
* @returns {Promise}
*/
_disposeVideoPreview() {
return this.state.previewVideoTrack
? this.state.previewVideoTrack.dispose() : Promise.resolve();
}
/**
* Returns what the title of the device selection modal should be.
*
* Note: This is temporary logic to appease design sooner. Device selection
* and all other settings will be combined into one modal.
*
* @returns {string}
*/
_getModalTitle() {
if (shouldShowOnlyDeviceSelection()) {
return 'settings.title';
}
return 'deviceSelection.deviceSettings';
}
/**
* Disposes preview tracks and signals to
* close DeviceSelectionDialogBase.
*
* @private
* @returns {boolean} Returns false to prevent closure until cleanup is
* complete.
*/
_onCancel() {
if (this._isClosing) {
return false;
}
this._isClosing = true;
const cleanupPromises = this._attemptPreviewTrackCleanup();
Promise.all(cleanupPromises)
.then(this.props.closeModal)
.catch(this.props.closeModal);
return false;
}
/**
* Identifies changes to the preferred input/output devices and perform
* necessary cleanup and requests to use those devices. Closes the modal
* after cleanup and device change requests complete.
*
* @private
* @returns {boolean} Returns false to prevent closure until cleanup is
* complete.
*/
_onSubmit() {
if (this._isClosing) {
return false;
}
this._isClosing = true;
this._attemptPreviewTrackCleanup()
.then(this._setDevicesAndClose, this._setDevicesAndClose);
return false;
}
/**
* Creates an AudioInputPreview for previewing if audio is being received.
* Null will be returned if local stats for tracking audio input levels
* cannot be obtained.
*
* @private
* @returns {ReactComponent|null}
*/
_renderAudioInputPreview() {
if (this.props.hideAudioInputPreview) {
return null;
}
return (
<AudioInputPreview
track = { this.state.previewAudioTrack } />
);
}
/**
* Creates an AudioOutputPreview instance for playing a test sound with the
* passed in device id. Null will be returned if hideAudioOutput is truthy.
*
* @private
* @returns {ReactComponent|null}
*/
_renderAudioOutputPreview() {
if (this.props.hideAudioOutputSelect) {
return null;
}
return (
<AudioOutputPreview
deviceId = { this.state.selectedAudioOutputId } />
);
}
/**
* Creates a DeviceSelector instance based on the passed in configuration.
*
* @private
* @param {Object} deviceSelectorProps - The props for the DeviceSelector.
* @returns {ReactElement}
*/
_renderSelector(deviceSelectorProps) {
return (
<div key = { deviceSelectorProps.label }>
<div className = 'device-selector-label'>
{ this.props.t(deviceSelectorProps.label) }
</div>
<DeviceSelector { ...deviceSelectorProps } />
</div>
);
}
/**
* Creates DeviceSelector instances for video output, audio input, and audio
* output.
*
* @private
* @returns {Array<ReactElement>} DeviceSelector instances.
*/
_renderSelectors() {
const { availableDevices } = this.props;
const configurations = [
{
devices: availableDevices.videoInput,
hasPermission: this.props.hasVideoPermission(),
icon: 'icon-camera',
isDisabled: this.props.disableDeviceChange,
key: 'videoInput',
label: 'settings.selectCamera',
onSelect: this._updateVideoInput,
selectedDeviceId: this.state.selectedVideoInputId
},
{
devices: availableDevices.audioInput,
hasPermission: this.props.hasAudioPermission(),
icon: 'icon-microphone',
isDisabled: this.props.disableAudioInputChange
|| this.props.disableDeviceChange,
key: 'audioInput',
label: 'settings.selectMic',
onSelect: this._updateAudioInput,
selectedDeviceId: this.state.selectedAudioInputId
}
];
if (!this.props.hideAudioOutputSelect) {
configurations.push({
devices: availableDevices.audioOutput,
hasPermission: this.props.hasAudioPermission()
|| this.props.hasVideoPermission(),
icon: 'icon-volume',
isDisabled: this.props.disableDeviceChange,
key: 'audioOutput',
label: 'settings.selectAudioOutput',
onSelect: this._updateAudioOutput,
selectedDeviceId: this.state.selectedAudioOutputId
});
}
return configurations.map(config => this._renderSelector(config));
}
/**
* Sets the selected devices and closes the dialog.
*
* @returns {void}
*/
_setDevicesAndClose() {
const {
setVideoInputDevice,
setAudioInputDevice,
setAudioOutputDevice,
closeModal
} = this.props;
const promises = [];
if (this.state.selectedVideoInputId
!== this.props.currentVideoInputId) {
promises.push(setVideoInputDevice(this.state.selectedVideoInputId));
}
if (this.state.selectedAudioInputId
!== this.props.currentAudioInputId) {
promises.push(setAudioInputDevice(this.state.selectedAudioInputId));
}
if (this.state.selectedAudioOutputId
!== this.props.currentAudioOutputId) {
promises.push(
setAudioOutputDevice(this.state.selectedAudioOutputId));
}
Promise.all(promises).then(closeModal, closeModal);
}
/**
* Callback invoked when a new audio input device has been selected. Updates
* the internal state of the user's selection as well as the audio track
* that should display in the preview.
*
* @param {string} deviceId - The id of the chosen audio input device.
* @private
* @returns {void}
*/
_updateAudioInput(deviceId) {
this.setState({
selectedAudioInputId: deviceId
}, () => {
this._disposeAudioPreview()
.then(() => createLocalTrack('audio', deviceId))
.then(jitsiLocalTrack => {
this.setState({
previewAudioTrack: jitsiLocalTrack
});
})
.catch(() => {
this.setState({
previewAudioTrack: null
});
});
});
}
/**
* Callback invoked when a new audio output device has been selected.
* Updates the internal state of the user's selection.
*
* @param {string} deviceId - The id of the chosen audio output device.
* @private
* @returns {void}
*/
_updateAudioOutput(deviceId) {
this.setState({
selectedAudioOutputId: deviceId
});
}
/**
* Callback invoked when a new video input device has been selected. Updates
* the internal state of the user's selection as well as the video track
* that should display in the preview.
*
* @param {string} deviceId - The id of the chosen video input device.
* @private
* @returns {void}
*/
_updateVideoInput(deviceId) {
this.setState({
selectedVideoInputId: deviceId
}, () => {
this._disposeVideoPreview()
.then(() => createLocalTrack('video', deviceId))
.then(jitsiLocalTrack => {
if (!jitsiLocalTrack) {
return Promise.reject();
}
this.setState({
previewVideoTrack: jitsiLocalTrack,
previewVideoTrackError: null
});
})
.catch(() => {
this.setState({
previewVideoTrack: null,
previewVideoTrackError:
this.props.t('deviceSelection.previewUnavailable')
});
});
});
}
}
export default translate(DeviceSelectionDialogBase);

View File

@ -1,3 +1,4 @@
export { default as DeviceSelectionDialog } from './DeviceSelectionDialog'; // @flow
export { default as DeviceSelectionDialogBase }
from './DeviceSelectionDialogBase'; export { default as DeviceSelection } from './DeviceSelection';
export type { Props as DeviceSelectionProps } from './DeviceSelection';

View File

@ -0,0 +1,35 @@
// @flow
import { getAudioOutputDeviceId } from '../base/devices';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { toState } from '../base/redux';
/**
* Returns the properties for the device selection dialog from Redux state.
*
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state.
* @returns {Object} - The properties for the device selection dialog.
*/
export function getDeviceSelectionDialogProps(stateful: Object | Function) {
const state = toState(stateful);
const settings = state['features/base/settings'];
return {
availableDevices: state['features/base/devices'],
disableAudioInputChange:
!JitsiMeetJS.isMultipleAudioInputSupported(),
disableDeviceChange:
!JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
hasAudioPermission: JitsiMeetJS.mediaDevices
.isDevicePermissionGranted.bind(null, 'audio'),
hasVideoPermission: JitsiMeetJS.mediaDevices
.isDevicePermissionGranted.bind(null, 'video'),
hideAudioInputPreview:
!JitsiMeetJS.isCollectingLocalStats(),
hideAudioOutputSelect: !JitsiMeetJS.mediaDevices
.isDeviceChangeAvailable('output'),
selectedAudioInputId: settings.micDeviceId,
selectedAudioOutputId: getAudioOutputDeviceId(),
selectedVideoInputId: settings.cameraDeviceId
};
}

View File

@ -1,6 +1,7 @@
export * from './actions'; export * from './actions';
export * from './actionTypes'; export * from './actionTypes';
export * from './components'; export * from './components';
export * from './functions';
import './middleware'; import './middleware';
import './reducer'; import './reducer';

View File

@ -11,8 +11,11 @@ import {
Transport Transport
} from '../../../modules/transport'; } from '../../../modules/transport';
import { parseURLParams } from '../base/config'; import { parseURLParams } from '../base/config';
import { DeviceSelection } from '../device-selection';
import DeviceSelectionDialogBase from './components/DeviceSelectionDialogBase'; // Using the full path to the file to prevent adding unnecessary code into the
// dialog popup bundle.
import DialogWithTabs from '../base/dialog/components/DialogWithTabs';
const logger = Logger.getLogger(__filename); const logger = Logger.getLogger(__filename);
@ -29,10 +32,9 @@ export default class DeviceSelectionPopup {
*/ */
constructor(i18next) { constructor(i18next) {
this.close = this.close.bind(this); this.close = this.close.bind(this);
this._setVideoInputDevice = this._setVideoInputDevice.bind(this);
this._setAudioInputDevice = this._setAudioInputDevice.bind(this);
this._setAudioOutputDevice = this._setAudioOutputDevice.bind(this);
this._i18next = i18next; this._i18next = i18next;
this._onSubmit = this._onSubmit.bind(this);
const { scope } = parseURLParams(window.location); const { scope } = parseURLParams(window.location);
this._transport = new Transport({ this._transport = new Transport({
@ -56,10 +58,11 @@ export default class DeviceSelectionPopup {
this._dialogProps = { this._dialogProps = {
availableDevices: {}, availableDevices: {},
currentAudioInputId: '', selectedAudioInputId: '',
currentAudioOutputId: '', selectedAudioOutputId: '',
currentVideoInputId: '', selectedVideoInputId: '',
disableAudioInputChange: true, disableAudioInputChange: true,
disableBlanketClickDismiss: true,
disableDeviceChange: true, disableDeviceChange: true,
hasAudioPermission: JitsiMeetJS.mediaDevices hasAudioPermission: JitsiMeetJS.mediaDevices
.isDevicePermissionGranted.bind(null, 'audio'), .isDevicePermissionGranted.bind(null, 'audio'),
@ -67,6 +70,7 @@ export default class DeviceSelectionPopup {
.isDevicePermissionGranted.bind(null, 'video'), .isDevicePermissionGranted.bind(null, 'video'),
hideAudioInputPreview: !JitsiMeetJS.isCollectingLocalStats(), hideAudioInputPreview: !JitsiMeetJS.isCollectingLocalStats(),
hideAudioOutputSelect: true hideAudioOutputSelect: true
}; };
this._initState(); this._initState();
} }
@ -153,9 +157,9 @@ export default class DeviceSelectionPopup {
]) => { ]) => {
this._changeDialogProps({ this._changeDialogProps({
availableDevices, availableDevices,
currentAudioInputId: currentDevices.audioInput, selectedAudioInputId: currentDevices.audioInput,
currentAudioOutputId: currentDevices.audioOutput, selectedAudioOutputId: currentDevices.audioOutput,
currentVideoInputId: currentDevices.videoInput, selectedVideoInputId: currentDevices.videoInput,
disableAudioInputChange: !multiAudioInputSupported, disableAudioInputChange: !multiAudioInputSupported,
disableDeviceChange: !listAvailable || !changeAvailable, disableDeviceChange: !listAvailable || !changeAvailable,
hideAudioOutputSelect: !changeOutputAvailable hideAudioOutputSelect: !changeOutputAvailable
@ -217,26 +221,58 @@ export default class DeviceSelectionPopup {
}); });
} }
/**
* Callback invoked to save changes to selected devices and close the
* dialog.
*
* @param {Object} newSettings - The chosen device IDs.
* @private
* @returns {void}
*/
_onSubmit(newSettings) {
const promises = [];
if (newSettings.selectedVideoInputId
!== this._dialogProps.selectedVideoInputId) {
promises.push(
this._setVideoInputDevice(newSettings.selectedVideoInputId));
}
if (newSettings.selectedAudioInputId
!== this._dialogProps.selectedAudioInputId) {
promises.push(
this._setAudioInputDevice(newSettings.selectedAudioInputId));
}
if (newSettings.selectedAudioOutputId
!== this._dialogProps.selectedAudioOutputId) {
promises.push(
this._setAudioOutputDevice(newSettings.selectedAudioOutputId));
}
Promise.all(promises).then(this.close, this.close);
}
/** /**
* Renders the React components for the popup page. * Renders the React components for the popup page.
* *
* @returns {void} * @returns {void}
*/ */
_render() { _render() {
const props = { const onSubmit = this.close;
...this._dialogProps,
closeModal: this.close,
disableBlanketClickDismiss: true,
setAudioInputDevice: this._setAudioInputDevice,
setAudioOutputDevice: this._setAudioOutputDevice,
setVideoInputDevice: this._setVideoInputDevice
};
ReactDOM.render( ReactDOM.render(
<I18nextProvider <I18nextProvider i18n = { this._i18next }>
i18n = { this._i18next }>
<AtlasKitThemeProvider mode = 'dark'> <AtlasKitThemeProvider mode = 'dark'>
<DeviceSelectionDialogBase { ...props } /> <DialogWithTabs
closeDialog = { this.close }
onSubmit = { onSubmit }
tabs = { [ {
component: DeviceSelection,
label: 'settings.devices',
props: this._dialogProps,
submit: this._onSubmit
} ] } />
</AtlasKitThemeProvider> </AtlasKitThemeProvider>
</I18nextProvider>, </I18nextProvider>,
document.getElementById('react')); document.getElementById('react'));

View File

@ -1,6 +1,14 @@
// @flow // @flow
import { setFollowMe, setStartMutedPolicy } from '../base/conference';
import { openDialog } from '../base/dialog';
import { i18next } from '../base/i18n';
import { SET_SETTINGS_VIEW_VISIBLE } from './actionTypes'; import { SET_SETTINGS_VIEW_VISIBLE } from './actionTypes';
import { SettingsDialog } from './components';
import { getMoreTabProps, getProfileTabProps } from './functions';
declare var APP: Object;
/** /**
* Sets the visibility of the view/UI which renders the app's settings. * Sets the visibility of the view/UI which renders the app's settings.
@ -18,3 +26,61 @@ export function setSettingsViewVisible(visible: boolean) {
visible visible
}; };
} }
/**
* Opens {@code SettingsDialog}.
*
* @param {string} defaultTab - The tab in {@code SettingsDialog} that should be
* displayed initially.
* @returns {Function}
*/
export function openSettingsDialog(defaultTab: string) {
return openDialog(SettingsDialog, { defaultTab });
}
/**
* Submits the settings from the "More" tab of the settings dialog.
*
* @param {Object} newState - The new settings.
* @returns {Function}
*/
export function submitMoreTab(newState: Object): Function {
return (dispatch, getState) => {
const currentState = getMoreTabProps(getState());
if (newState.followMeEnabled !== currentState.followMeEnabled) {
dispatch(setFollowMe(newState.followMeEnabled));
}
if (newState.startAudioMuted !== currentState.startAudioMuted
|| newState.startVideoMuted !== currentState.startVideoMuted) {
dispatch(setStartMutedPolicy(
newState.startAudioMuted, newState.startVideoMuted));
}
if (newState.currentLanguage !== currentState.currentLanguage) {
i18next.changeLanguage(newState.currentLanguage);
}
};
}
/**
* Submits the settings from the "Profile" tab of the settings dialog.
*
* @param {Object} newState - The new settings.
* @returns {Function}
*/
export function submitProfileTab(newState: Object): Function {
return (dispatch, getState) => {
const currentState = getProfileTabProps(getState());
if (newState.displayName !== currentState.displayName) {
APP.conference.changeLocalDisplayName(newState.displayName);
}
if (newState.email !== currentState.email) {
APP.conference.changeLocalEmail(newState.email);
}
};
}

View File

@ -1,87 +0,0 @@
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

@ -1,179 +0,0 @@
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

@ -1,199 +0,0 @@
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,239 @@
// @flow
import { CheckboxGroup, CheckboxStateless } from '@atlaskit/checkbox';
import DropdownMenu, {
DropdownItem,
DropdownItemGroup
} from '@atlaskit/dropdown-menu';
import React from 'react';
import { AbstractDialogTab } from '../../../base/dialog';
import type { Props as AbstractDialogTabProps } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
/**
* The type of the React {@code Component} props of {@link MoreTab}.
*/
export type Props = {
...$Exact<AbstractDialogTabProps>,
/**
* The currently selected language to display in the language select
* dropdown.
*/
currentLanguage: string,
/**
* Whether or not the user has selected the Follow Me feature to be enabled.
*/
followMeEnabled: boolean,
/**
* All available languages to display in the language select dropdown.
*/
languages: Array<string>,
/**
* Whether or not to display the language select dropdown.
*/
showLanguageSettings: boolean,
/**
* Whether or not to display moderator-only settings.
*/
showModeratorSettings: boolean,
/**
* Whether or not the user has selected the Start Audio Muted feature to be
* enabled.
*/
startAudioMuted: boolean,
/**
* Whether or not the user has selected the Start Video Muted feature to be
* enabled.
*/
startVideoMuted: boolean,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* The type of the React {@code Component} state of {@link MoreTab}.
*/
type State = {
/**
* Whether or not the language select dropdown is open.
*/
isLanguageSelectOpen: boolean
};
/**
* React {@code Component} for modifying language and moderator settings.
*
* @extends Component
*/
class MoreTab extends AbstractDialogTab<Props, State> {
/**
* Initializes a new {@code MoreTab} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.state = {
isLanguageSelectOpen: false
};
// Bind event handler so it is only bound once for every instance.
this._onLanguageDropdownOpenChange
= this._onLanguageDropdownOpenChange.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { showModeratorSettings, showLanguageSettings } = this.props;
const content = [];
if (showModeratorSettings) {
content.push(this._renderModeratorSettings());
}
if (showLanguageSettings) {
content.push(this._renderLangaugeSelect());
}
return <div className = 'more-tab'>{ content }</div>;
}
_onLanguageDropdownOpenChange: (Object) => void;
/**
* Callback invoked to toggle display of the language select dropdown.
*
* @param {Object} event - The event for opening or closing the dropdown.
* @private
* @returns {void}
*/
_onLanguageDropdownOpenChange({ isOpen }) {
this.setState({ isLanguageSelectOpen: isOpen });
}
/**
* Returns the menu item for changing displayed language.
*
* @private
* @returns {ReactElement}
*/
_renderLangaugeSelect() {
const {
currentLanguage,
languages,
t
} = this.props;
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 = {
() => super._onChange({ currentLanguage: language }) }>
{ t(`languages:${language}`) }
</DropdownItem>
);
return (
<div
className = 'settings-sub-pane language-settings'
key = 'language'>
<div className = 'mock-atlaskit-label'>
{ t('settings.language') }
</div>
<DropdownMenu
isOpen = { this.state.isLanguageSelectOpen }
onOpenChange = { this._onLanguageDropdownOpenChange }
shouldFitContainer = { true }
trigger = { currentLanguage
? t(`languages:${currentLanguage}`)
: '' }
triggerButtonProps = {{
appearance: 'primary',
shouldFitContainer: true
}}
triggerType = 'button'>
<DropdownItemGroup>
{ languageItems }
</DropdownItemGroup>
</DropdownMenu>
</div>
);
}
/**
* Returns the React Element for modifying conference-wide settings.
*
* @private
* @returns {ReactElement}
*/
_renderModeratorSettings() {
const {
followMeEnabled,
startAudioMuted,
startVideoMuted,
t
} = this.props;
return (
<div
className = 'settings-sub-pane'
key = 'moderator'>
<div className = 'mock-atlaskit-label'>
{ t('settings.moderator') }
</div>
<CheckboxGroup>
<CheckboxStateless
isChecked = { startAudioMuted }
label = { t('settings.startAudioMuted') }
name = 'start-audio-muted'
// eslint-disable-next-line react/jsx-no-bind
onChange = {
({ target: { checked } }) =>
super._onChange({ startAudioMuted: checked })
} />
<CheckboxStateless
isChecked = { startVideoMuted }
label = { t('settings.startVideoMuted') }
name = 'start-video-muted'
// eslint-disable-next-line react/jsx-no-bind
onChange = {
({ target: { checked } }) =>
super._onChange({ startVideoMuted: checked })
} />
<CheckboxStateless
isChecked = { followMeEnabled }
label = { t('settings.followMe') }
name = 'follow-me'
// eslint-disable-next-line react/jsx-no-bind
onChange = {
({ target: { checked } }) =>
super._onChange({ followMeEnabled: checked })
} />
</CheckboxGroup>
</div>
);
}
}
export default translate(MoreTab);

View File

@ -0,0 +1,188 @@
// @flow
import Button from '@atlaskit/button';
import { FieldTextStateless } from '@atlaskit/field-text';
import React from 'react';
import { AbstractDialogTab } from '../../../base/dialog';
import type { Props as AbstractDialogTabProps } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import UIEvents from '../../../../../service/UI/UIEvents';
import {
sendAnalytics,
createProfilePanelButtonEvent
} from '../../../analytics';
declare var APP: Object;
/**
* The type of the React {@code Component} props of {@link ProfileTab}.
*/
export type Props = {
...$Exact<AbstractDialogTabProps>,
/**
* Whether or not server-side authentication is available.
*/
authEnabled: boolean,
/**
* The name of the currently (server-side) authenticated user.
*/
authLogin: string,
/**
* The display name to display for the local participant.
*/
displayName: string,
/**
* The email to display for the local participant.
*/
email: string,
/**
* Invoked to obtain translated strings.
*/
t: Function
}
/**
* React {@code Component} for modifying the local user's profile.
*
* @extends Component
*/
class ProfileTab extends AbstractDialogTab<Props> {
/**
* Initializes a new {@code ConnectedSettingsDialog} instance.
*
* @param {Props} props - The React {@code Component} props to initialize
* the new {@code ConnectedSettingsDialog} instance with.
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._onAuthToggle = this._onAuthToggle.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const {
authEnabled,
displayName,
email,
t
} = this.props;
return (
<div>
<div className = 'profile-edit'>
<div className = 'profile-edit-field'>
<FieldTextStateless
autoFocus = { true }
compact = { true }
id = 'setDisplayName'
label = { t('profile.setDisplayNameLabel') }
// eslint-disable-next-line react/jsx-no-bind
onChange = {
({ target: { value } }) =>
super._onChange({ displayName: value })
}
placeholder = { t('settings.name') }
shouldFitContainer = { true }
type = 'text'
value = { displayName } />
</div>
<div className = 'profile-edit-field'>
<FieldTextStateless
compact = { true }
id = 'setEmail'
label = { t('profile.setEmailLabel') }
// eslint-disable-next-line react/jsx-no-bind
onChange = {
({ target: { value } }) =>
super._onChange({ email: value })
}
placeholder = { t('profile.setEmailInput') }
shouldFitContainer = { true }
type = 'text'
value = { email } />
</div>
</div>
{ authEnabled && this._renderAuth() }
</div>
);
}
_onAuthToggle: () => void;
/**
* Shows the dialog for logging in or out of a server and closes this
* dialog.
*
* @private
* @returns {void}
*/
_onAuthToggle() {
if (this.props.authLogin) {
sendAnalytics(createProfilePanelButtonEvent('logout.button'));
APP.UI.messageHandler.openTwoButtonDialog({
leftButtonKey: 'dialog.Yes',
msgKey: 'dialog.logoutQuestion',
submitFunction(evt, yes) {
if (yes) {
APP.UI.emitEvent(UIEvents.LOGOUT);
}
},
titleKey: 'dialog.logoutTitle'
});
} else {
sendAnalytics(createProfilePanelButtonEvent('login.button'));
APP.UI.emitEvent(UIEvents.AUTH_CLICKED);
}
this.props.closeDialog();
}
/**
* Returns a React Element for interacting with server-side authentication.
*
* @private
* @returns {ReactElement}
*/
_renderAuth() {
const {
authLogin,
t
} = this.props;
return (
<div>
<div className = 'mock-atlaskit-label'>
{ t('toolbar.authenticate') }
</div>
{ authLogin
&& <div className = 'auth-name'>
{ t('settings.loggedIn', { name: authLogin }) }
</div> }
<Button
appearance = 'primary'
id = 'login_button'
onClick = { this._onAuthToggle }
type = 'button'>
{ authLogin ? t('toolbar.logout') : t('toolbar.login') }
</Button>
</div>
);
}
}
export default translate(ProfileTab);

View File

@ -6,8 +6,10 @@ import { createToolbarEvent, sendAnalytics } from '../../../analytics';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { AbstractButton } from '../../../base/toolbox'; import { AbstractButton } from '../../../base/toolbox';
import type { AbstractButtonProps } from '../../../base/toolbox'; import type { AbstractButtonProps } from '../../../base/toolbox';
import { openDeviceSelectionDialog } from '../../../device-selection'; import { openDeviceSelectionPopup } from '../../../device-selection';
import { toggleSettings } from '../../../side-panel';
import { openSettingsDialog } from '../../actions';
import { SETTINGS_TABS } from '../../constants';
declare var interfaceConfig: Object; declare var interfaceConfig: Object;
@ -21,11 +23,6 @@ type Props = AbstractButtonProps & {
*/ */
_filmstripOnly: boolean, _filmstripOnly: boolean,
/**
* Array containing the enabled settings sections.
*/
_sections: Array<string>,
/** /**
* The redux {@code dispatch} function. * The redux {@code dispatch} function.
*/ */
@ -48,14 +45,13 @@ class SettingsButton extends AbstractButton<Props, *> {
* @returns {void} * @returns {void}
*/ */
_handleClick() { _handleClick() {
const { _filmstripOnly, _sections, dispatch } = this.props; const { _filmstripOnly, dispatch } = this.props;
sendAnalytics(createToolbarEvent('settings')); sendAnalytics(createToolbarEvent('settings'));
if (_filmstripOnly if (_filmstripOnly) {
|| (_sections.length === 1 && _sections.includes('devices'))) { dispatch(openDeviceSelectionPopup());
dispatch(openDeviceSelectionDialog());
} else { } else {
dispatch(toggleSettings()); dispatch(openSettingsDialog(SETTINGS_TABS.DEVICES));
} }
} }
} }
@ -75,8 +71,7 @@ function _mapStateToProps(state): Object { // eslint-disable-line no-unused-vars
// interfaceConfig is part of redux we will. // interfaceConfig is part of redux we will.
return { return {
_filmstripOnly: Boolean(interfaceConfig.filmStripOnly), _filmstripOnly: Boolean(interfaceConfig.filmStripOnly)
_sections: interfaceConfig.SETTINGS_SECTIONS || []
}; };
} }

View File

@ -0,0 +1,167 @@
// @flow
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { DialogWithTabs, hideDialog } from '../../../base/dialog';
import {
DeviceSelection,
getDeviceSelectionDialogProps,
submitDeviceSelectionTab
} from '../../../device-selection';
import MoreTab from './MoreTab';
import ProfileTab from './ProfileTab';
import { getMoreTabProps, getProfileTabProps } from '../../functions';
import { submitMoreTab, submitProfileTab } from '../../actions';
import { SETTINGS_TABS } from '../../constants';
declare var APP: Object;
declare var interfaceConfig: Object;
/**
* The type of the React {@code Component} props of
* {@link ConnectedSettingsDialog}.
*/
type Props = {
/**
* Which settings tab should be initially displayed. If not defined then
* the first tab will be displayed.
*/
defaultTab: string,
/**
* Information about the tabs to be rendered.
*/
_tabs: Array<Object>,
/**
* Invoked to save changed settings.
*/
dispatch: Function,
};
/**
* A React {@code Component} for displaying a dialog to modify local settings
* and conference-wide (moderator) settings. This version is connected to
* redux to get the current settings.
*
* @extends Component
*/
class SettingsDialog extends Component<Props> {
/**
* Initializes a new {@code ConnectedSettingsDialog} instance.
*
* @param {Props} props - The React {@code Component} props to initialize
* the new {@code ConnectedSettingsDialog} instance with.
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._closeDialog = this._closeDialog.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _tabs, defaultTab, dispatch } = this.props;
const onSubmit = this._closeDialog;
const defaultTabIdx
= _tabs.findIndex(({ name }) => name === defaultTab);
const tabs = _tabs.map(tab => {
return {
...tab,
submit: (...args) => dispatch(tab.submit(...args))
};
});
return (
<DialogWithTabs
closeDialog = { this._closeDialog }
defaultTab = {
defaultTabIdx === -1 ? undefined : defaultTabIdx
}
onSubmit = { onSubmit }
tabs = { tabs } />
);
}
_closeDialog: () => void;
/**
* Callback invoked to close the dialog without saving changes.
*
* @private
* @returns {void}
*/
_closeDialog() {
this.props.dispatch(hideDialog());
}
}
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code ConnectedSettingsDialog} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* tabs: Array<Object>
* }}
*/
function _mapStateToProps(state) {
const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
const jwt = state['features/base/jwt'];
// The settings sections to display.
const showDeviceSettings = configuredTabs.includes('devices');
const moreTabProps = getMoreTabProps(state);
const { showModeratorSettings, showLanguageSettings } = moreTabProps;
const showProfileSettings
= configuredTabs.includes('profile') && jwt.isGuest;
const tabs = [];
if (showDeviceSettings) {
tabs.push({
name: SETTINGS_TABS.DEVICES,
component: DeviceSelection,
label: 'settings.devices',
props: getDeviceSelectionDialogProps(state),
styles: 'settings-pane devices-pane',
submit: submitDeviceSelectionTab
});
}
if (showProfileSettings) {
tabs.push({
name: SETTINGS_TABS.PROFILE,
component: ProfileTab,
label: 'profile.title',
props: getProfileTabProps(state),
styles: 'settings-pane profile-pane',
submit: submitProfileTab
});
}
if (showModeratorSettings || showLanguageSettings) {
tabs.push({
name: SETTINGS_TABS.MORE,
component: MoreTab,
label: 'settings.more',
props: moreTabProps,
styles: 'settings-pane more-pane',
submit: submitMoreTab
});
}
return { _tabs: tabs };
}
export default connect(_mapStateToProps)(SettingsDialog);

View File

@ -1,111 +0,0 @@
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

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

View File

@ -0,0 +1,5 @@
export const SETTINGS_TABS = {
DEVICES: 'devices_tab',
MORE: 'more_tab',
PROFILE: 'profile_tab'
};

View File

@ -1,6 +1,8 @@
// @flow // @flow
import { toState } from '../base/redux';
import { parseStandardURIString } from '../base/util'; import { parseStandardURIString } from '../base/util';
import { i18next, DEFAULT_LANGUAGE, LANGUAGES } from '../base/i18n';
import { getLocalParticipant, PARTICIPANT_ROLE } from '../base/participants';
declare var interfaceConfig: Object; declare var interfaceConfig: Object;
@ -60,3 +62,57 @@ export function shouldShowOnlyDeviceSelection() {
return interfaceConfig.SETTINGS_SECTIONS.length === 1 return interfaceConfig.SETTINGS_SECTIONS.length === 1
&& isSettingEnabled('devices'); && isSettingEnabled('devices');
} }
/**
* Returns the properties for the "More" tab from settings dialog from Redux
* state.
*
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state.
* @returns {Object} - The properties for the "More" tab from settings dialog.
*/
export function getMoreTabProps(stateful: Object | Function) {
const state = toState(stateful);
const language = i18next.language || DEFAULT_LANGUAGE;
const conference = state['features/base/conference'];
const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
const localParticipant = getLocalParticipant(state);
// The settings sections to display.
const showModeratorSettings
= configuredTabs.includes('moderator')
&& localParticipant.role === PARTICIPANT_ROLE.MODERATOR;
return {
currentLanguage: language,
followMeEnabled: Boolean(conference.followMeEnabled),
languages: LANGUAGES,
showLanguageSettings: configuredTabs.includes('language'),
showModeratorSettings,
startAudioMuted: Boolean(conference.startAudioMutedPolicy),
startVideoMuted: Boolean(conference.startVideoMutedPolicy)
};
}
/**
* Returns the properties for the "Profile" tab from settings dialog from Redux
* state.
*
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state.
* @returns {Object} - The properties for the "Profile" tab from settings
* dialog.
*/
export function getProfileTabProps(stateful: Object | Function) {
const state = toState(stateful);
const conference = state['features/base/conference'];
const localParticipant = getLocalParticipant(state);
return {
authEnabled: conference.authEnabled,
authLogin: conference.authLogin,
displayName: localParticipant.name,
email: localParticipant.email
};
}

View File

@ -1,6 +1,7 @@
export * from './actions'; export * from './actions';
export * from './actionTypes'; export * from './actionTypes';
export * from './components'; export * from './components';
export * from './constants';
export * from './functions'; export * from './functions';
import './middleware'; import './middleware';

View File

@ -27,23 +27,3 @@ export const SET_VISIBLE_PANEL = Symbol('SET_VISIBLE_PANEL');
* } * }
*/ */
export const TOGGLE_CHAT = Symbol('TOGGLE_CHAT'); export const TOGGLE_CHAT = Symbol('TOGGLE_CHAT');
/**
* The type of the action which signals to toggle the display of profile editing
* in the side panel.
*
* {
* type: TOGGLE_PROFILE
* }
*/
export const TOGGLE_PROFILE = Symbol('TOGGLE_PROFILE');
/**
* The type of the action which signals to toggle the display of settings in the
* side panel.
*
* {
* type: TOGGLE_SETTINGS
* }
*/
export const TOGGLE_SETTINGS = Symbol('TOGGLE_SETTINGS');

View File

@ -1,9 +1,7 @@
import { import {
CLOSE_PANEL, CLOSE_PANEL,
SET_VISIBLE_PANEL, SET_VISIBLE_PANEL,
TOGGLE_CHAT, TOGGLE_CHAT
TOGGLE_PROFILE,
TOGGLE_SETTINGS
} from './actionTypes'; } from './actionTypes';
/** /**
@ -49,29 +47,3 @@ export function toggleChat() {
type: TOGGLE_CHAT type: TOGGLE_CHAT
}; };
} }
/**
* Toggles display of the profile side panel.
*
* @returns {{
* type: TOGGLE_PROFILE
* }}
*/
export function toggleProfile() {
return {
type: TOGGLE_PROFILE
};
}
/**
* Toggles display of the settings side panel.
*
* @returns {{
* type: TOGGLE_SETTINGS
* }}
*/
export function toggleSettings() {
return {
type: TOGGLE_SETTINGS
};
}

View File

@ -2,12 +2,7 @@
import { MiddlewareRegistry } from '../base/redux'; import { MiddlewareRegistry } from '../base/redux';
import { import { CLOSE_PANEL, TOGGLE_CHAT } from './actionTypes';
CLOSE_PANEL,
TOGGLE_CHAT,
TOGGLE_PROFILE,
TOGGLE_SETTINGS
} from './actionTypes';
declare var APP: Object; declare var APP: Object;
@ -31,14 +26,6 @@ MiddlewareRegistry.register(store => next => action => {
case TOGGLE_CHAT: case TOGGLE_CHAT:
APP.UI.toggleChat(); APP.UI.toggleChat();
break; break;
case TOGGLE_PROFILE:
APP.UI.toggleSidePanel('profile_container');
break;
case TOGGLE_SETTINGS:
APP.UI.toggleSidePanel('settings_container');
break;
} }
return next(action); return next(action);

View File

@ -114,6 +114,7 @@ function _mapStateToProps(state) {
return { return {
_localParticipant: getLocalParticipant(state), _localParticipant: getLocalParticipant(state),
_unclickable: !state['features/base/jwt'].isGuest _unclickable: !state['features/base/jwt'].isGuest
|| !interfaceConfig.SETTINGS_SECTIONS.includes('profile')
}; };
} }

View File

@ -36,9 +36,13 @@ import {
StopRecordingDialog, StopRecordingDialog,
getActiveSession getActiveSession
} from '../../../recording'; } from '../../../recording';
import { SettingsButton } from '../../../settings'; import {
SETTINGS_TABS,
SettingsButton,
openSettingsDialog
} from '../../../settings';
import { toggleSharedVideo } from '../../../shared-video'; import { toggleSharedVideo } from '../../../shared-video';
import { toggleChat, toggleProfile } from '../../../side-panel'; import { toggleChat } from '../../../side-panel';
import { SpeakerStats } from '../../../speaker-stats'; import { SpeakerStats } from '../../../speaker-stats';
import { import {
OverflowMenuVideoQualityItem, OverflowMenuVideoQualityItem,
@ -515,7 +519,7 @@ class Toolbox extends Component<Props> {
* @returns {void} * @returns {void}
*/ */
_doToggleProfile() { _doToggleProfile() {
this.props.dispatch(toggleProfile()); this.props.dispatch(openSettingsDialog(SETTINGS_TABS.PROFILE));
} }
/** /**

View File

@ -36,12 +36,6 @@ export default {
*/ */
TOGGLE_AUDIO_ONLY: 'UI.toggle_audioonly', TOGGLE_AUDIO_ONLY: 'UI.toggle_audioonly',
TOGGLE_CHAT: 'UI.toggle_chat', TOGGLE_CHAT: 'UI.toggle_chat',
TOGGLE_SETTINGS: 'UI.toggle_settings',
/**
* Notifies that the profile toolbar button has been clicked.
*/
TOGGLE_PROFILE: 'UI.toggle_profile',
/** /**
* Notifies that a command to toggle the filmstrip has been issued. The * Notifies that a command to toggle the filmstrip has been issued. The

View File

@ -146,7 +146,7 @@ module.exports = [
], ],
'device_selection_popup_bundle': 'device_selection_popup_bundle':
'./react/features/device-selection/popup.js', './react/features/settings/popup.js',
'alwaysontop': 'alwaysontop':
'./react/features/always-on-top/index.js', './react/features/always-on-top/index.js',