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:
parent
0acc9187ed
commit
1f8fa3b6d4
|
@ -33,6 +33,7 @@ import EventEmitter from 'events';
|
|||
import {
|
||||
AVATAR_ID_COMMAND,
|
||||
AVATAR_URL_COMMAND,
|
||||
authStatusChanged,
|
||||
conferenceFailed,
|
||||
conferenceJoined,
|
||||
conferenceLeft,
|
||||
|
@ -1650,7 +1651,7 @@ export default {
|
|||
room.on(
|
||||
JitsiConferenceEvents.AUTH_STATUS_CHANGED,
|
||||
(authEnabled, authLogin) =>
|
||||
APP.UI.updateAuthInfo(authEnabled, authLogin));
|
||||
APP.store.dispatch(authStatusChanged(authEnabled, authLogin)));
|
||||
|
||||
room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED,
|
||||
user => APP.UI.onUserFeaturesChanged(user));
|
||||
|
@ -1997,7 +1998,6 @@ export default {
|
|||
id: from,
|
||||
email: data.value
|
||||
}));
|
||||
APP.UI.setUserEmail(from, data.value);
|
||||
});
|
||||
|
||||
room.addCommandListener(
|
||||
|
@ -2575,7 +2575,6 @@ export default {
|
|||
email: formattedEmail
|
||||
}));
|
||||
|
||||
APP.UI.setUserEmail(localId, formattedEmail);
|
||||
sendData(commands.EMAIL, formattedEmail);
|
||||
},
|
||||
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
/**
|
||||
* Titles and subtitles of inner containers.
|
||||
*/
|
||||
div.title, div.subTitle {
|
||||
div.title {
|
||||
margin: 24px 0 11px;
|
||||
}
|
||||
|
||||
|
@ -112,53 +112,4 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
@import 'modals/dialog';
|
||||
@import 'modals/feedback/feedback';
|
||||
@import 'modals/invite/info';
|
||||
@import 'modals/settings/settings';
|
||||
@import 'modals/speaker_stats/speaker_stats';
|
||||
@import 'modals/video-quality/video-quality';
|
||||
@import 'videolayout_default';
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -51,7 +51,7 @@ var interfaceConfig = {
|
|||
'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
|
||||
// screen, 'height' would fit the original video height to the height of the
|
||||
|
|
|
@ -163,11 +163,15 @@
|
|||
"selectMic": "Microphone",
|
||||
"selectAudioOutput": "Audio output",
|
||||
"followMe": "Everyone follows me",
|
||||
"language": "Language",
|
||||
"loggedIn": "Logged in as __name__",
|
||||
"noDevice": "None",
|
||||
"cameraAndMic": "Camera and microphone",
|
||||
"moderator": "MODERATOR",
|
||||
"moderator": "Moderator",
|
||||
"more": "More",
|
||||
"password": "SET PASSWORD",
|
||||
"audioVideo": "AUDIO AND VIDEO"
|
||||
"audioVideo": "AUDIO AND VIDEO",
|
||||
"devices": "Devices"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Profile",
|
||||
|
|
|
@ -15,11 +15,7 @@ import SharedVideoManager from './shared_video/SharedVideo';
|
|||
|
||||
import VideoLayout from './videolayout/VideoLayout';
|
||||
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 { JitsiTrackErrors } from '../../react/features/base/lib-jitsi-meet';
|
||||
import {
|
||||
|
@ -33,7 +29,6 @@ import {
|
|||
setNotificationsEnabled,
|
||||
showWarningNotification
|
||||
} from '../../react/features/notifications';
|
||||
import { shouldShowOnlyDeviceSelection } from '../../react/features/settings';
|
||||
import {
|
||||
dockToolbox,
|
||||
setToolboxEnabled,
|
||||
|
@ -97,22 +92,6 @@ const UIListeners = new Map([
|
|||
], [
|
||||
UIEvents.TOGGLE_CHAT,
|
||||
() => 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,
|
||||
() => UI.handleToggleFilmstrip()
|
||||
|
@ -216,7 +195,6 @@ UI.changeDisplayName = function(id, displayName) {
|
|||
VideoLayout.onDisplayNameChanged(id, displayName);
|
||||
|
||||
if (APP.conference.isLocalId(id) || id === 'localVideoContainer') {
|
||||
Profile.changeDisplayName(displayName);
|
||||
Chat.setChatConversationMode(Boolean(displayName));
|
||||
}
|
||||
};
|
||||
|
@ -268,7 +246,7 @@ UI.setLocalRaisedHandStatus
|
|||
*/
|
||||
UI.initConference = function() {
|
||||
const { getState } = APP.store;
|
||||
const { email, id, name } = getLocalParticipant(getState);
|
||||
const { id, name } = getLocalParticipant(getState);
|
||||
|
||||
// Update default button states before showing the toolbar
|
||||
// if local role changes buttons state will be again updated.
|
||||
|
@ -282,11 +260,6 @@ UI.initConference = function() {
|
|||
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
|
||||
// other participants' UI. Consequently, it needs (1) read and write access
|
||||
// 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));
|
||||
}
|
||||
|
||||
// Configure avatar
|
||||
UI.setUserEmail(id);
|
||||
|
||||
// set initial display name
|
||||
if (displayName) {
|
||||
UI.changeDisplayName(id, displayName);
|
||||
|
@ -739,17 +709,6 @@ UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout));
|
|||
// Used by torture.
|
||||
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.
|
||||
*
|
||||
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import Chat from './chat/Chat';
|
||||
import SettingsMenu from './settings/SettingsMenu';
|
||||
import Profile from './profile/Profile';
|
||||
import { isButtonEnabled } from '../../../react/features/toolbox';
|
||||
|
||||
const SidePanels = {
|
||||
|
@ -9,16 +7,6 @@ const SidePanels = {
|
|||
if (isButtonEnabled('chat')) {
|
||||
Chat.init(eventEmitter);
|
||||
}
|
||||
|
||||
// Initialize settings
|
||||
if (isButtonEnabled('settings')) {
|
||||
SettingsMenu.init(eventEmitter);
|
||||
}
|
||||
|
||||
// Initialize profile
|
||||
if (isButtonEnabled('profile')) {
|
||||
Profile.init(eventEmitter);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -43,6 +43,10 @@ const htmlStr = `
|
|||
function initHTML() {
|
||||
$(`#${sidePanelsContainerId}`)
|
||||
.append(htmlStr);
|
||||
|
||||
// make sure we translate the panel, as adding it can be after i18n
|
||||
// library had initialized and translated already present html
|
||||
APP.translation.translateElement($(`#${sidePanelsContainerId}`));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 : '');
|
||||
}
|
||||
};
|
|
@ -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'));
|
||||
}
|
||||
};
|
|
@ -37,6 +37,94 @@
|
|||
"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": {
|
||||
"version": "3.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/dropdown-menu/-/dropdown-menu-3.10.2.tgz",
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"dependencies": {
|
||||
"@atlaskit/avatar": "8.0.5",
|
||||
"@atlaskit/button": "5.4.2",
|
||||
"@atlaskit/checkbox": "2.0.2",
|
||||
"@atlaskit/dropdown-menu": "3.10.2",
|
||||
"@atlaskit/droplist": "4.11.1",
|
||||
"@atlaskit/field-text": "4.0.1",
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -22,6 +22,7 @@ import { getLocalTracks, trackAdded, trackRemoved } from '../tracks';
|
|||
import { getJitsiMeetGlobalNS } from '../util';
|
||||
|
||||
import {
|
||||
AUTH_STATUS_CHANGED,
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
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.
|
||||
*
|
||||
|
|
|
@ -6,6 +6,7 @@ import { assign, ReducerRegistry, set } from '../redux';
|
|||
import { LOCKED_LOCALLY, LOCKED_REMOTELY } from '../../room-lock';
|
||||
|
||||
import {
|
||||
AUTH_STATUS_CHANGED,
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_LEFT,
|
||||
|
@ -31,6 +32,9 @@ import { isRoomValid } from './functions';
|
|||
*/
|
||||
ReducerRegistry.register('features/base/conference', (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case AUTH_STATUS_CHANGED:
|
||||
return _authStatusChanged(state, action);
|
||||
|
||||
case CONFERENCE_FAILED:
|
||||
return _conferenceFailed(state, action);
|
||||
|
||||
|
@ -85,6 +89,23 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
|
|||
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
|
||||
* base/conference.
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
|
@ -1,4 +1,9 @@
|
|||
// @flow
|
||||
|
||||
export { default as BottomSheet } from './BottomSheet';
|
||||
export { default as DialogContainer } from './DialogContainer';
|
||||
export { default as Dialog } from './Dialog';
|
||||
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';
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/* globals APP, interfaceConfig */
|
||||
|
||||
import { API_ID } from '../../../modules/API/constants';
|
||||
import {
|
||||
PostMessageTransportBackend,
|
||||
|
@ -12,66 +10,18 @@ import {
|
|||
setAudioOutputDevice,
|
||||
setVideoInputDevice
|
||||
} from '../base/devices';
|
||||
import { openDialog } from '../base/dialog';
|
||||
import { i18next } from '../base/i18n';
|
||||
import JitsiMeetJS from '../base/lib-jitsi-meet';
|
||||
|
||||
import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
|
||||
import { DeviceSelectionDialog } from './components';
|
||||
|
||||
/**
|
||||
* 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')
|
||||
}));
|
||||
});
|
||||
}
|
||||
import { getDeviceSelectionDialogProps } from './functions';
|
||||
|
||||
/**
|
||||
* Opens a popup window with the device selection dialog in it.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
function _openDeviceSelectionDialogInPopup() {
|
||||
export function openDeviceSelectionPopup() {
|
||||
return (dispatch, getState) => {
|
||||
const { popupDialogData } = getState()['features/device-selection'];
|
||||
|
||||
|
@ -218,3 +168,36 @@ function _setDeviceSelectionPopupData(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));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -1,3 +1,4 @@
|
|||
export { default as DeviceSelectionDialog } from './DeviceSelectionDialog';
|
||||
export { default as DeviceSelectionDialogBase }
|
||||
from './DeviceSelectionDialogBase';
|
||||
// @flow
|
||||
|
||||
export { default as DeviceSelection } from './DeviceSelection';
|
||||
export type { Props as DeviceSelectionProps } from './DeviceSelection';
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './components';
|
||||
export * from './functions';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
|
|
|
@ -11,8 +11,11 @@ import {
|
|||
Transport
|
||||
} from '../../../modules/transport';
|
||||
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);
|
||||
|
||||
|
@ -29,10 +32,9 @@ export default class DeviceSelectionPopup {
|
|||
*/
|
||||
constructor(i18next) {
|
||||
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._onSubmit = this._onSubmit.bind(this);
|
||||
|
||||
const { scope } = parseURLParams(window.location);
|
||||
|
||||
this._transport = new Transport({
|
||||
|
@ -56,10 +58,11 @@ export default class DeviceSelectionPopup {
|
|||
|
||||
this._dialogProps = {
|
||||
availableDevices: {},
|
||||
currentAudioInputId: '',
|
||||
currentAudioOutputId: '',
|
||||
currentVideoInputId: '',
|
||||
selectedAudioInputId: '',
|
||||
selectedAudioOutputId: '',
|
||||
selectedVideoInputId: '',
|
||||
disableAudioInputChange: true,
|
||||
disableBlanketClickDismiss: true,
|
||||
disableDeviceChange: true,
|
||||
hasAudioPermission: JitsiMeetJS.mediaDevices
|
||||
.isDevicePermissionGranted.bind(null, 'audio'),
|
||||
|
@ -67,6 +70,7 @@ export default class DeviceSelectionPopup {
|
|||
.isDevicePermissionGranted.bind(null, 'video'),
|
||||
hideAudioInputPreview: !JitsiMeetJS.isCollectingLocalStats(),
|
||||
hideAudioOutputSelect: true
|
||||
|
||||
};
|
||||
this._initState();
|
||||
}
|
||||
|
@ -153,9 +157,9 @@ export default class DeviceSelectionPopup {
|
|||
]) => {
|
||||
this._changeDialogProps({
|
||||
availableDevices,
|
||||
currentAudioInputId: currentDevices.audioInput,
|
||||
currentAudioOutputId: currentDevices.audioOutput,
|
||||
currentVideoInputId: currentDevices.videoInput,
|
||||
selectedAudioInputId: currentDevices.audioInput,
|
||||
selectedAudioOutputId: currentDevices.audioOutput,
|
||||
selectedVideoInputId: currentDevices.videoInput,
|
||||
disableAudioInputChange: !multiAudioInputSupported,
|
||||
disableDeviceChange: !listAvailable || !changeAvailable,
|
||||
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.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_render() {
|
||||
const props = {
|
||||
...this._dialogProps,
|
||||
closeModal: this.close,
|
||||
disableBlanketClickDismiss: true,
|
||||
setAudioInputDevice: this._setAudioInputDevice,
|
||||
setAudioOutputDevice: this._setAudioOutputDevice,
|
||||
setVideoInputDevice: this._setVideoInputDevice
|
||||
};
|
||||
const onSubmit = this.close;
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nextProvider
|
||||
i18n = { this._i18next }>
|
||||
<I18nextProvider i18n = { this._i18next }>
|
||||
<AtlasKitThemeProvider mode = 'dark'>
|
||||
<DeviceSelectionDialogBase { ...props } />
|
||||
<DialogWithTabs
|
||||
closeDialog = { this.close }
|
||||
onSubmit = { onSubmit }
|
||||
tabs = { [ {
|
||||
component: DeviceSelection,
|
||||
label: 'settings.devices',
|
||||
props: this._dialogProps,
|
||||
submit: this._onSubmit
|
||||
} ] } />
|
||||
</AtlasKitThemeProvider>
|
||||
</I18nextProvider>,
|
||||
document.getElementById('react'));
|
|
@ -1,6 +1,14 @@
|
|||
// @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 { 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.
|
||||
|
@ -18,3 +26,61 @@ export function setSettingsViewVisible(visible: boolean) {
|
|||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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));
|
|
@ -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);
|
|
@ -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));
|
|
@ -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);
|
|
@ -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);
|
|
@ -6,8 +6,10 @@ import { createToolbarEvent, sendAnalytics } from '../../../analytics';
|
|||
import { translate } from '../../../base/i18n';
|
||||
import { AbstractButton } from '../../../base/toolbox';
|
||||
import type { AbstractButtonProps } from '../../../base/toolbox';
|
||||
import { openDeviceSelectionDialog } from '../../../device-selection';
|
||||
import { toggleSettings } from '../../../side-panel';
|
||||
import { openDeviceSelectionPopup } from '../../../device-selection';
|
||||
|
||||
import { openSettingsDialog } from '../../actions';
|
||||
import { SETTINGS_TABS } from '../../constants';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
|
@ -21,11 +23,6 @@ type Props = AbstractButtonProps & {
|
|||
*/
|
||||
_filmstripOnly: boolean,
|
||||
|
||||
/**
|
||||
* Array containing the enabled settings sections.
|
||||
*/
|
||||
_sections: Array<string>,
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
|
@ -48,14 +45,13 @@ class SettingsButton extends AbstractButton<Props, *> {
|
|||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { _filmstripOnly, _sections, dispatch } = this.props;
|
||||
const { _filmstripOnly, dispatch } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent('settings'));
|
||||
if (_filmstripOnly
|
||||
|| (_sections.length === 1 && _sections.includes('devices'))) {
|
||||
dispatch(openDeviceSelectionDialog());
|
||||
if (_filmstripOnly) {
|
||||
dispatch(openDeviceSelectionPopup());
|
||||
} 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.
|
||||
|
||||
return {
|
||||
_filmstripOnly: Boolean(interfaceConfig.filmStripOnly),
|
||||
_sections: interfaceConfig.SETTINGS_SECTIONS || []
|
||||
_filmstripOnly: Boolean(interfaceConfig.filmStripOnly)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
|
@ -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));
|
|
@ -1,2 +1,2 @@
|
|||
export { default as SettingsButton } from './SettingsButton';
|
||||
export { default as SettingsMenu } from './SettingsMenu';
|
||||
export { default as SettingsDialog } from './SettingsDialog';
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export const SETTINGS_TABS = {
|
||||
DEVICES: 'devices_tab',
|
||||
MORE: 'more_tab',
|
||||
PROFILE: 'profile_tab'
|
||||
};
|
|
@ -1,6 +1,8 @@
|
|||
// @flow
|
||||
|
||||
import { toState } from '../base/redux';
|
||||
import { parseStandardURIString } from '../base/util';
|
||||
import { i18next, DEFAULT_LANGUAGE, LANGUAGES } from '../base/i18n';
|
||||
import { getLocalParticipant, PARTICIPANT_ROLE } from '../base/participants';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
|
@ -60,3 +62,57 @@ export function shouldShowOnlyDeviceSelection() {
|
|||
return interfaceConfig.SETTINGS_SECTIONS.length === 1
|
||||
&& 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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './components';
|
||||
export * from './constants';
|
||||
export * from './functions';
|
||||
|
||||
import './middleware';
|
||||
|
|
|
@ -27,23 +27,3 @@ export const SET_VISIBLE_PANEL = Symbol('SET_VISIBLE_PANEL');
|
|||
* }
|
||||
*/
|
||||
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');
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import {
|
||||
CLOSE_PANEL,
|
||||
SET_VISIBLE_PANEL,
|
||||
TOGGLE_CHAT,
|
||||
TOGGLE_PROFILE,
|
||||
TOGGLE_SETTINGS
|
||||
TOGGLE_CHAT
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
|
@ -49,29 +47,3 @@ export function toggleChat() {
|
|||
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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,12 +2,7 @@
|
|||
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
CLOSE_PANEL,
|
||||
TOGGLE_CHAT,
|
||||
TOGGLE_PROFILE,
|
||||
TOGGLE_SETTINGS
|
||||
} from './actionTypes';
|
||||
import { CLOSE_PANEL, TOGGLE_CHAT } from './actionTypes';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
|
@ -31,14 +26,6 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
case TOGGLE_CHAT:
|
||||
APP.UI.toggleChat();
|
||||
break;
|
||||
|
||||
case TOGGLE_PROFILE:
|
||||
APP.UI.toggleSidePanel('profile_container');
|
||||
break;
|
||||
|
||||
case TOGGLE_SETTINGS:
|
||||
APP.UI.toggleSidePanel('settings_container');
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
|
|
|
@ -114,6 +114,7 @@ function _mapStateToProps(state) {
|
|||
return {
|
||||
_localParticipant: getLocalParticipant(state),
|
||||
_unclickable: !state['features/base/jwt'].isGuest
|
||||
|| !interfaceConfig.SETTINGS_SECTIONS.includes('profile')
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -36,9 +36,13 @@ import {
|
|||
StopRecordingDialog,
|
||||
getActiveSession
|
||||
} from '../../../recording';
|
||||
import { SettingsButton } from '../../../settings';
|
||||
import {
|
||||
SETTINGS_TABS,
|
||||
SettingsButton,
|
||||
openSettingsDialog
|
||||
} from '../../../settings';
|
||||
import { toggleSharedVideo } from '../../../shared-video';
|
||||
import { toggleChat, toggleProfile } from '../../../side-panel';
|
||||
import { toggleChat } from '../../../side-panel';
|
||||
import { SpeakerStats } from '../../../speaker-stats';
|
||||
import {
|
||||
OverflowMenuVideoQualityItem,
|
||||
|
@ -515,7 +519,7 @@ class Toolbox extends Component<Props> {
|
|||
* @returns {void}
|
||||
*/
|
||||
_doToggleProfile() {
|
||||
this.props.dispatch(toggleProfile());
|
||||
this.props.dispatch(openSettingsDialog(SETTINGS_TABS.PROFILE));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -36,12 +36,6 @@ export default {
|
|||
*/
|
||||
TOGGLE_AUDIO_ONLY: 'UI.toggle_audioonly',
|
||||
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
|
||||
|
|
|
@ -146,7 +146,7 @@ module.exports = [
|
|||
],
|
||||
|
||||
'device_selection_popup_bundle':
|
||||
'./react/features/device-selection/popup.js',
|
||||
'./react/features/settings/popup.js',
|
||||
|
||||
'alwaysontop':
|
||||
'./react/features/always-on-top/index.js',
|
||||
|
|
Loading…
Reference in New Issue