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 {
|
import {
|
||||||
AVATAR_ID_COMMAND,
|
AVATAR_ID_COMMAND,
|
||||||
AVATAR_URL_COMMAND,
|
AVATAR_URL_COMMAND,
|
||||||
|
authStatusChanged,
|
||||||
conferenceFailed,
|
conferenceFailed,
|
||||||
conferenceJoined,
|
conferenceJoined,
|
||||||
conferenceLeft,
|
conferenceLeft,
|
||||||
|
@ -1650,7 +1651,7 @@ export default {
|
||||||
room.on(
|
room.on(
|
||||||
JitsiConferenceEvents.AUTH_STATUS_CHANGED,
|
JitsiConferenceEvents.AUTH_STATUS_CHANGED,
|
||||||
(authEnabled, authLogin) =>
|
(authEnabled, authLogin) =>
|
||||||
APP.UI.updateAuthInfo(authEnabled, authLogin));
|
APP.store.dispatch(authStatusChanged(authEnabled, authLogin)));
|
||||||
|
|
||||||
room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED,
|
room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED,
|
||||||
user => APP.UI.onUserFeaturesChanged(user));
|
user => APP.UI.onUserFeaturesChanged(user));
|
||||||
|
@ -1997,7 +1998,6 @@ export default {
|
||||||
id: from,
|
id: from,
|
||||||
email: data.value
|
email: data.value
|
||||||
}));
|
}));
|
||||||
APP.UI.setUserEmail(from, data.value);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
room.addCommandListener(
|
room.addCommandListener(
|
||||||
|
@ -2575,7 +2575,6 @@ export default {
|
||||||
email: formattedEmail
|
email: formattedEmail
|
||||||
}));
|
}));
|
||||||
|
|
||||||
APP.UI.setUserEmail(localId, formattedEmail);
|
|
||||||
sendData(commands.EMAIL, formattedEmail);
|
sendData(commands.EMAIL, formattedEmail);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -92,7 +92,7 @@
|
||||||
/**
|
/**
|
||||||
* Titles and subtitles of inner containers.
|
* Titles and subtitles of inner containers.
|
||||||
*/
|
*/
|
||||||
div.title, div.subTitle {
|
div.title {
|
||||||
margin: 24px 0 11px;
|
margin: 24px 0 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,53 +112,4 @@
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-menu {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding-left: 10%;
|
|
||||||
padding-right: 10%;
|
|
||||||
|
|
||||||
.moderator-checkbox {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 5px 0;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.moderator-option {
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subTitle {
|
|
||||||
color: $defaultSideBarFontColor;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Profile
|
|
||||||
*/
|
|
||||||
.auth_container {
|
|
||||||
ul {
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
li {
|
|
||||||
list-style-type: none;
|
|
||||||
|
|
||||||
a.authButton {
|
|
||||||
width: 160px;
|
|
||||||
margin: 10px 20px;
|
|
||||||
padding: 3px 29px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background-color: #06a5df;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: $defaultColor;
|
|
||||||
text-decoration: none;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
@import 'modals/dialog';
|
@import 'modals/dialog';
|
||||||
@import 'modals/feedback/feedback';
|
@import 'modals/feedback/feedback';
|
||||||
@import 'modals/invite/info';
|
@import 'modals/invite/info';
|
||||||
|
@import 'modals/settings/settings';
|
||||||
@import 'modals/speaker_stats/speaker_stats';
|
@import 'modals/speaker_stats/speaker_stats';
|
||||||
@import 'modals/video-quality/video-quality';
|
@import 'modals/video-quality/video-quality';
|
||||||
@import 'videolayout_default';
|
@import 'videolayout_default';
|
||||||
|
|
|
@ -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'
|
'invite', 'feedback', 'stats', 'shortcuts'
|
||||||
],
|
],
|
||||||
|
|
||||||
SETTINGS_SECTIONS: [ 'language', 'devices', 'moderator' ],
|
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile' ],
|
||||||
|
|
||||||
// Determines how the video would fit the screen. 'both' would fit the whole
|
// Determines how the video would fit the screen. 'both' would fit the whole
|
||||||
// screen, 'height' would fit the original video height to the height of the
|
// screen, 'height' would fit the original video height to the height of the
|
||||||
|
|
|
@ -163,11 +163,15 @@
|
||||||
"selectMic": "Microphone",
|
"selectMic": "Microphone",
|
||||||
"selectAudioOutput": "Audio output",
|
"selectAudioOutput": "Audio output",
|
||||||
"followMe": "Everyone follows me",
|
"followMe": "Everyone follows me",
|
||||||
|
"language": "Language",
|
||||||
|
"loggedIn": "Logged in as __name__",
|
||||||
"noDevice": "None",
|
"noDevice": "None",
|
||||||
"cameraAndMic": "Camera and microphone",
|
"cameraAndMic": "Camera and microphone",
|
||||||
"moderator": "MODERATOR",
|
"moderator": "Moderator",
|
||||||
|
"more": "More",
|
||||||
"password": "SET PASSWORD",
|
"password": "SET PASSWORD",
|
||||||
"audioVideo": "AUDIO AND VIDEO"
|
"audioVideo": "AUDIO AND VIDEO",
|
||||||
|
"devices": "Devices"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"title": "Profile",
|
"title": "Profile",
|
||||||
|
|
|
@ -15,11 +15,7 @@ import SharedVideoManager from './shared_video/SharedVideo';
|
||||||
|
|
||||||
import VideoLayout from './videolayout/VideoLayout';
|
import VideoLayout from './videolayout/VideoLayout';
|
||||||
import Filmstrip from './videolayout/Filmstrip';
|
import Filmstrip from './videolayout/Filmstrip';
|
||||||
import Profile from './side_pannels/profile/Profile';
|
|
||||||
|
|
||||||
import {
|
|
||||||
openDeviceSelectionDialog
|
|
||||||
} from '../../react/features/device-selection';
|
|
||||||
import { updateDeviceList } from '../../react/features/base/devices';
|
import { updateDeviceList } from '../../react/features/base/devices';
|
||||||
import { JitsiTrackErrors } from '../../react/features/base/lib-jitsi-meet';
|
import { JitsiTrackErrors } from '../../react/features/base/lib-jitsi-meet';
|
||||||
import {
|
import {
|
||||||
|
@ -33,7 +29,6 @@ import {
|
||||||
setNotificationsEnabled,
|
setNotificationsEnabled,
|
||||||
showWarningNotification
|
showWarningNotification
|
||||||
} from '../../react/features/notifications';
|
} from '../../react/features/notifications';
|
||||||
import { shouldShowOnlyDeviceSelection } from '../../react/features/settings';
|
|
||||||
import {
|
import {
|
||||||
dockToolbox,
|
dockToolbox,
|
||||||
setToolboxEnabled,
|
setToolboxEnabled,
|
||||||
|
@ -97,22 +92,6 @@ const UIListeners = new Map([
|
||||||
], [
|
], [
|
||||||
UIEvents.TOGGLE_CHAT,
|
UIEvents.TOGGLE_CHAT,
|
||||||
() => UI.toggleChat()
|
() => UI.toggleChat()
|
||||||
], [
|
|
||||||
UIEvents.TOGGLE_SETTINGS,
|
|
||||||
() => {
|
|
||||||
// Opening of device selection is special-cased as it is a dialog
|
|
||||||
// opened through a button in settings and not directly displayed in
|
|
||||||
// settings itself. As it is not useful to only have a settings menu
|
|
||||||
// with a button to open a dialog, open the dialog directly instead.
|
|
||||||
if (shouldShowOnlyDeviceSelection()) {
|
|
||||||
APP.store.dispatch(openDeviceSelectionDialog());
|
|
||||||
} else {
|
|
||||||
UI.toggleSidePanel('settings_container');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
], [
|
|
||||||
UIEvents.TOGGLE_PROFILE,
|
|
||||||
() => UI.toggleSidePanel('profile_container')
|
|
||||||
], [
|
], [
|
||||||
UIEvents.TOGGLE_FILMSTRIP,
|
UIEvents.TOGGLE_FILMSTRIP,
|
||||||
() => UI.handleToggleFilmstrip()
|
() => UI.handleToggleFilmstrip()
|
||||||
|
@ -216,7 +195,6 @@ UI.changeDisplayName = function(id, displayName) {
|
||||||
VideoLayout.onDisplayNameChanged(id, displayName);
|
VideoLayout.onDisplayNameChanged(id, displayName);
|
||||||
|
|
||||||
if (APP.conference.isLocalId(id) || id === 'localVideoContainer') {
|
if (APP.conference.isLocalId(id) || id === 'localVideoContainer') {
|
||||||
Profile.changeDisplayName(displayName);
|
|
||||||
Chat.setChatConversationMode(Boolean(displayName));
|
Chat.setChatConversationMode(Boolean(displayName));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -268,7 +246,7 @@ UI.setLocalRaisedHandStatus
|
||||||
*/
|
*/
|
||||||
UI.initConference = function() {
|
UI.initConference = function() {
|
||||||
const { getState } = APP.store;
|
const { getState } = APP.store;
|
||||||
const { email, id, name } = getLocalParticipant(getState);
|
const { id, name } = getLocalParticipant(getState);
|
||||||
|
|
||||||
// Update default button states before showing the toolbar
|
// Update default button states before showing the toolbar
|
||||||
// if local role changes buttons state will be again updated.
|
// if local role changes buttons state will be again updated.
|
||||||
|
@ -282,11 +260,6 @@ UI.initConference = function() {
|
||||||
UI.changeDisplayName('localVideoContainer', displayName);
|
UI.changeDisplayName('localVideoContainer', displayName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we configure our avatar id, before creating avatar for us
|
|
||||||
if (email) {
|
|
||||||
UI.setUserEmail(id, email);
|
|
||||||
}
|
|
||||||
|
|
||||||
// FollowMe attempts to copy certain aspects of the moderator's UI into the
|
// FollowMe attempts to copy certain aspects of the moderator's UI into the
|
||||||
// other participants' UI. Consequently, it needs (1) read and write access
|
// other participants' UI. Consequently, it needs (1) read and write access
|
||||||
// to the UI (depending on the moderator role of the local participant) and
|
// to the UI (depending on the moderator role of the local participant) and
|
||||||
|
@ -492,9 +465,6 @@ UI.addUser = function(user) {
|
||||||
APP.store.dispatch(showParticipantJoinedNotification(displayName));
|
APP.store.dispatch(showParticipantJoinedNotification(displayName));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure avatar
|
|
||||||
UI.setUserEmail(id);
|
|
||||||
|
|
||||||
// set initial display name
|
// set initial display name
|
||||||
if (displayName) {
|
if (displayName) {
|
||||||
UI.changeDisplayName(id, displayName);
|
UI.changeDisplayName(id, displayName);
|
||||||
|
@ -739,17 +709,6 @@ UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout));
|
||||||
// Used by torture.
|
// Used by torture.
|
||||||
UI.dockToolbar = dock => APP.store.dispatch(dockToolbox(dock));
|
UI.dockToolbar = dock => APP.store.dispatch(dockToolbox(dock));
|
||||||
|
|
||||||
/**
|
|
||||||
* Update user email.
|
|
||||||
* @param {string} id user id
|
|
||||||
* @param {string} email user email
|
|
||||||
*/
|
|
||||||
UI.setUserEmail = function(id, email) {
|
|
||||||
if (APP.conference.isLocalId(id)) {
|
|
||||||
Profile.changeEmail(email);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the displayed avatar for participant.
|
* Updates the displayed avatar for participant.
|
||||||
*
|
*
|
||||||
|
@ -880,25 +839,6 @@ UI.notifyFocusDisconnected = function(focus, retrySec) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates auth info on the UI.
|
|
||||||
* @param {boolean} isAuthEnabled if authentication is enabled
|
|
||||||
* @param {string} [login] current login
|
|
||||||
*/
|
|
||||||
UI.updateAuthInfo = function(isAuthEnabled, login) {
|
|
||||||
const showAuth = isAuthEnabled && UIUtil.isAuthenticationEnabled();
|
|
||||||
const loggedIn = Boolean(login);
|
|
||||||
|
|
||||||
Profile.showAuthenticationButtons(showAuth);
|
|
||||||
|
|
||||||
if (showAuth) {
|
|
||||||
Profile.setAuthenticatedIdentity(login);
|
|
||||||
|
|
||||||
Profile.showLoginButton(!loggedIn);
|
|
||||||
Profile.showLogoutButton(loggedIn);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies interested listeners that the raise hand property has changed.
|
* Notifies interested listeners that the raise hand property has changed.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import Chat from './chat/Chat';
|
import Chat from './chat/Chat';
|
||||||
import SettingsMenu from './settings/SettingsMenu';
|
|
||||||
import Profile from './profile/Profile';
|
|
||||||
import { isButtonEnabled } from '../../../react/features/toolbox';
|
import { isButtonEnabled } from '../../../react/features/toolbox';
|
||||||
|
|
||||||
const SidePanels = {
|
const SidePanels = {
|
||||||
|
@ -9,16 +7,6 @@ const SidePanels = {
|
||||||
if (isButtonEnabled('chat')) {
|
if (isButtonEnabled('chat')) {
|
||||||
Chat.init(eventEmitter);
|
Chat.init(eventEmitter);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize settings
|
|
||||||
if (isButtonEnabled('settings')) {
|
|
||||||
SettingsMenu.init(eventEmitter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize profile
|
|
||||||
if (isButtonEnabled('profile')) {
|
|
||||||
Profile.init(eventEmitter);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,10 @@ const htmlStr = `
|
||||||
function initHTML() {
|
function initHTML() {
|
||||||
$(`#${sidePanelsContainerId}`)
|
$(`#${sidePanelsContainerId}`)
|
||||||
.append(htmlStr);
|
.append(htmlStr);
|
||||||
|
|
||||||
|
// make sure we translate the panel, as adding it can be after i18n
|
||||||
|
// library had initialized and translated already present html
|
||||||
|
APP.translation.translateElement($(`#${sidePanelsContainerId}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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"
|
"styled-components": "^1.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@atlaskit/checkbox": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@atlaskit/checkbox/-/checkbox-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-sX5vixywY61A7Q/YR04g3Z+PhaZNEQ1orc/t2JX80iF+FLpEfvD1KY0ywraSHx5ljXjxJ9u6Df4az/y9BDmQ5A==",
|
||||||
|
"requires": {
|
||||||
|
"@atlaskit/button": "7.2.5",
|
||||||
|
"@atlaskit/icon": "11.3.0",
|
||||||
|
"@atlaskit/theme": "3.2.2",
|
||||||
|
"babel-runtime": "6.26.0",
|
||||||
|
"prop-types": "15.6.0",
|
||||||
|
"styled-components": "1.4.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@atlaskit/analytics-next": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@atlaskit/analytics-next/-/analytics-next-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-rAd+kLEEXZ8mu39qOf+E0kZu+k1RWNwd5vyfb0WXuBxq+jyCzAg19vhj99Uq2TblW/WgOX9ajBE+lEgbYqmvNw==",
|
||||||
|
"requires": {
|
||||||
|
"prop-types": "15.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@atlaskit/button": {
|
||||||
|
"version": "7.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@atlaskit/button/-/button-7.2.5.tgz",
|
||||||
|
"integrity": "sha512-YDH2wWxoMe9uGmyMy+zPQMbxkK0TrFplutu6bZ0n8Ojet8XcGOBPyWS5lf3Nt+DOPKLUXCI4pknR6fB6ZF1e/g==",
|
||||||
|
"requires": {
|
||||||
|
"@atlaskit/analytics-next": "2.1.2",
|
||||||
|
"@atlaskit/spinner": "5.0.2",
|
||||||
|
"@atlaskit/theme": "3.2.2",
|
||||||
|
"babel-runtime": "6.26.0",
|
||||||
|
"styled-components": "1.4.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@atlaskit/icon": {
|
||||||
|
"version": "11.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@atlaskit/icon/-/icon-11.3.0.tgz",
|
||||||
|
"integrity": "sha512-dFnpk3yT9EZUmCC8bUOP4WmENWMqLYezBOpv+mp/vKBbzT786c+ZVyDW5wZ9hSKmfb+aHjiZt+UuwUiVW5D+Wg==",
|
||||||
|
"requires": {
|
||||||
|
"@atlaskit/theme": "3.2.2",
|
||||||
|
"babel-runtime": "6.26.0",
|
||||||
|
"styled-components": "1.4.6",
|
||||||
|
"uuid": "3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@atlaskit/spinner": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@atlaskit/spinner/-/spinner-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-n0j/urjG3FF9q/6Nae981GwsdvT44zAobPqFGnaeKDfqUzrFHcs1PmL0dqa36aFJzOPZHzl6ZfBl9Q3Vpl9PKQ==",
|
||||||
|
"requires": {
|
||||||
|
"@atlaskit/theme": "3.2.2",
|
||||||
|
"babel-runtime": "6.26.0",
|
||||||
|
"react-transition-group": "2.3.1",
|
||||||
|
"styled-components": "1.4.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@atlaskit/theme": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-SQYgGe8WnO1aF991XraCzbMzOf8v1rMBkLYkwVb6BAjxTVgeSepMTVYOTQ7+KXzSAKFP0fDxgXnUB/VsSUR8Ig==",
|
||||||
|
"requires": {
|
||||||
|
"prop-types": "15.6.0",
|
||||||
|
"styled-components": "1.4.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"react-transition-group": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-hu4/LAOFSKjWt1+1hgnOv3ldxmt6lvZGTWz4KUkFrqzXrNDIVSu6txIcPszw7PNduR8en9YTN55JLRyd/L1ZiQ==",
|
||||||
|
"requires": {
|
||||||
|
"dom-helpers": "3.3.1",
|
||||||
|
"loose-envify": "1.3.1",
|
||||||
|
"prop-types": "15.6.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": {
|
||||||
|
"version": "15.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz",
|
||||||
|
"integrity": "sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ==",
|
||||||
|
"requires": {
|
||||||
|
"fbjs": "0.8.16",
|
||||||
|
"loose-envify": "1.3.1",
|
||||||
|
"object-assign": "4.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@atlaskit/dropdown-menu": {
|
"@atlaskit/dropdown-menu": {
|
||||||
"version": "3.10.2",
|
"version": "3.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/@atlaskit/dropdown-menu/-/dropdown-menu-3.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@atlaskit/dropdown-menu/-/dropdown-menu-3.10.2.tgz",
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atlaskit/avatar": "8.0.5",
|
"@atlaskit/avatar": "8.0.5",
|
||||||
"@atlaskit/button": "5.4.2",
|
"@atlaskit/button": "5.4.2",
|
||||||
|
"@atlaskit/checkbox": "2.0.2",
|
||||||
"@atlaskit/dropdown-menu": "3.10.2",
|
"@atlaskit/dropdown-menu": "3.10.2",
|
||||||
"@atlaskit/droplist": "4.11.1",
|
"@atlaskit/droplist": "4.11.1",
|
||||||
"@atlaskit/field-text": "4.0.1",
|
"@atlaskit/field-text": "4.0.1",
|
||||||
|
|
|
@ -1,3 +1,15 @@
|
||||||
|
/**
|
||||||
|
* The type of (redux) action which signals that server authentication has
|
||||||
|
* becoming available or unavailable or logged in user has changed.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: AUTH_STATUS_CHANGED,
|
||||||
|
* authEnabled: boolean,
|
||||||
|
* authLogin: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const AUTH_STATUS_CHANGED = Symbol('AUTH_STATUS_CHANGED');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of (redux) action which signals that a specific conference failed.
|
* The type of (redux) action which signals that a specific conference failed.
|
||||||
*
|
*
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { getLocalTracks, trackAdded, trackRemoved } from '../tracks';
|
||||||
import { getJitsiMeetGlobalNS } from '../util';
|
import { getJitsiMeetGlobalNS } from '../util';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AUTH_STATUS_CHANGED,
|
||||||
CONFERENCE_FAILED,
|
CONFERENCE_FAILED,
|
||||||
CONFERENCE_JOINED,
|
CONFERENCE_JOINED,
|
||||||
CONFERENCE_LEFT,
|
CONFERENCE_LEFT,
|
||||||
|
@ -178,6 +179,26 @@ function _addConferenceListeners(conference, dispatch) {
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the current known state of server-side authentication.
|
||||||
|
*
|
||||||
|
* @param {boolean} authEnabled - Whether or not server authentication is
|
||||||
|
* enabled.
|
||||||
|
* @param {string} authLogin - The current name of the logged in user, if any.
|
||||||
|
* @returns {{
|
||||||
|
* type: AUTH_STATUS_CHANGED,
|
||||||
|
* authEnabled: boolean,
|
||||||
|
* authLogin: string
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function authStatusChanged(authEnabled: boolean, authLogin: string) {
|
||||||
|
return {
|
||||||
|
type: AUTH_STATUS_CHANGED,
|
||||||
|
authEnabled,
|
||||||
|
authLogin
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signals that a specific conference has failed.
|
* Signals that a specific conference has failed.
|
||||||
*
|
*
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { assign, ReducerRegistry, set } from '../redux';
|
||||||
import { LOCKED_LOCALLY, LOCKED_REMOTELY } from '../../room-lock';
|
import { LOCKED_LOCALLY, LOCKED_REMOTELY } from '../../room-lock';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AUTH_STATUS_CHANGED,
|
||||||
CONFERENCE_FAILED,
|
CONFERENCE_FAILED,
|
||||||
CONFERENCE_JOINED,
|
CONFERENCE_JOINED,
|
||||||
CONFERENCE_LEFT,
|
CONFERENCE_LEFT,
|
||||||
|
@ -31,6 +32,9 @@ import { isRoomValid } from './functions';
|
||||||
*/
|
*/
|
||||||
ReducerRegistry.register('features/base/conference', (state = {}, action) => {
|
ReducerRegistry.register('features/base/conference', (state = {}, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
case AUTH_STATUS_CHANGED:
|
||||||
|
return _authStatusChanged(state, action);
|
||||||
|
|
||||||
case CONFERENCE_FAILED:
|
case CONFERENCE_FAILED:
|
||||||
return _conferenceFailed(state, action);
|
return _conferenceFailed(state, action);
|
||||||
|
|
||||||
|
@ -85,6 +89,23 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
|
||||||
return state;
|
return state;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduces a specific Redux action AUTH_STATUS_CHANGED of the feature
|
||||||
|
* base/conference.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state of the feature base/conference.
|
||||||
|
* @param {Action} action - The Redux action AUTH_STATUS_CHANGED to reduce.
|
||||||
|
* @private
|
||||||
|
* @returns {Object} The new state of the feature base/conference after the
|
||||||
|
* reduction of the specified action.
|
||||||
|
*/
|
||||||
|
function _authStatusChanged(state, { authEnabled, authLogin }) {
|
||||||
|
return assign(state, {
|
||||||
|
authEnabled,
|
||||||
|
authLogin
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reduces a specific Redux action CONFERENCE_FAILED of the feature
|
* Reduces a specific Redux action CONFERENCE_FAILED of the feature
|
||||||
* base/conference.
|
* base/conference.
|
||||||
|
|
|
@ -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 BottomSheet } from './BottomSheet';
|
||||||
export { default as DialogContainer } from './DialogContainer';
|
export { default as DialogContainer } from './DialogContainer';
|
||||||
export { default as Dialog } from './Dialog';
|
export { default as Dialog } from './Dialog';
|
||||||
export { default as StatelessDialog } from './StatelessDialog';
|
export { default as StatelessDialog } from './StatelessDialog';
|
||||||
|
export { default as DialogWithTabs } from './DialogWithTabs';
|
||||||
|
export { default as AbstractDialogTab } from './AbstractDialogTab';
|
||||||
|
export type { Props as AbstractDialogTabProps } from './AbstractDialogTab';
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
/* globals APP, interfaceConfig */
|
|
||||||
|
|
||||||
import { API_ID } from '../../../modules/API/constants';
|
import { API_ID } from '../../../modules/API/constants';
|
||||||
import {
|
import {
|
||||||
PostMessageTransportBackend,
|
PostMessageTransportBackend,
|
||||||
|
@ -12,66 +10,18 @@ import {
|
||||||
setAudioOutputDevice,
|
setAudioOutputDevice,
|
||||||
setVideoInputDevice
|
setVideoInputDevice
|
||||||
} from '../base/devices';
|
} from '../base/devices';
|
||||||
import { openDialog } from '../base/dialog';
|
|
||||||
import { i18next } from '../base/i18n';
|
import { i18next } from '../base/i18n';
|
||||||
import JitsiMeetJS from '../base/lib-jitsi-meet';
|
import JitsiMeetJS from '../base/lib-jitsi-meet';
|
||||||
|
|
||||||
import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
|
import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
|
||||||
import { DeviceSelectionDialog } from './components';
|
import { getDeviceSelectionDialogProps } from './functions';
|
||||||
|
|
||||||
/**
|
|
||||||
* Open DeviceSelectionDialog with a configuration based on the environment's
|
|
||||||
* supported abilities.
|
|
||||||
*
|
|
||||||
* @returns {Function}
|
|
||||||
*/
|
|
||||||
export function openDeviceSelectionDialog() {
|
|
||||||
return dispatch => {
|
|
||||||
if (interfaceConfig.filmStripOnly) {
|
|
||||||
dispatch(_openDeviceSelectionDialogInPopup());
|
|
||||||
} else {
|
|
||||||
dispatch(_openDeviceSelectionDialogHere());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the DeviceSelectionDialog in the same window.
|
|
||||||
*
|
|
||||||
* @returns {Function}
|
|
||||||
*/
|
|
||||||
function _openDeviceSelectionDialogHere() {
|
|
||||||
return dispatch =>
|
|
||||||
JitsiMeetJS.mediaDevices.isDeviceListAvailable()
|
|
||||||
.then(isDeviceListAvailable => {
|
|
||||||
const settings = APP.store.getState()['features/base/settings'];
|
|
||||||
|
|
||||||
dispatch(openDialog(DeviceSelectionDialog, {
|
|
||||||
currentAudioInputId: settings.micDeviceId,
|
|
||||||
currentAudioOutputId: getAudioOutputDeviceId(),
|
|
||||||
currentVideoInputId: settings.cameraDeviceId,
|
|
||||||
disableAudioInputChange:
|
|
||||||
!JitsiMeetJS.isMultipleAudioInputSupported(),
|
|
||||||
disableDeviceChange: !isDeviceListAvailable
|
|
||||||
|| !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
|
|
||||||
hasAudioPermission: JitsiMeetJS.mediaDevices
|
|
||||||
.isDevicePermissionGranted.bind(null, 'audio'),
|
|
||||||
hasVideoPermission: JitsiMeetJS.mediaDevices
|
|
||||||
.isDevicePermissionGranted.bind(null, 'video'),
|
|
||||||
hideAudioInputPreview:
|
|
||||||
!JitsiMeetJS.isCollectingLocalStats(),
|
|
||||||
hideAudioOutputSelect: !JitsiMeetJS.mediaDevices
|
|
||||||
.isDeviceChangeAvailable('output')
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a popup window with the device selection dialog in it.
|
* Opens a popup window with the device selection dialog in it.
|
||||||
*
|
*
|
||||||
* @returns {Function}
|
* @returns {Function}
|
||||||
*/
|
*/
|
||||||
function _openDeviceSelectionDialogInPopup() {
|
export function openDeviceSelectionPopup() {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const { popupDialogData } = getState()['features/device-selection'];
|
const { popupDialogData } = getState()['features/device-selection'];
|
||||||
|
|
||||||
|
@ -218,3 +168,36 @@ function _setDeviceSelectionPopupData(popupDialogData) {
|
||||||
popupDialogData
|
popupDialogData
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits the settings related to device selection.
|
||||||
|
*
|
||||||
|
* @param {Object} newState - The new settings.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
export function submitDeviceSelectionTab(newState) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const currentState = getDeviceSelectionDialogProps(getState());
|
||||||
|
|
||||||
|
if (newState.selectedVideoInputId
|
||||||
|
&& newState.selectedVideoInputId
|
||||||
|
!== currentState.selectedVideoInputId) {
|
||||||
|
dispatch(
|
||||||
|
setVideoInputDevice(newState.selectedVideoInputId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newState.selectedAudioInputId
|
||||||
|
&& newState.selectedAudioInputId
|
||||||
|
!== currentState.selectedAudioInputId) {
|
||||||
|
dispatch(
|
||||||
|
setAudioInputDevice(newState.selectedAudioInputId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newState.selectedAudioOutputId
|
||||||
|
&& newState.selectedAudioOutputId
|
||||||
|
!== currentState.selectedAudioOutputId) {
|
||||||
|
dispatch(
|
||||||
|
setAudioOutputDevice(newState.selectedAudioOutputId));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -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';
|
// @flow
|
||||||
export { default as DeviceSelectionDialogBase }
|
|
||||||
from './DeviceSelectionDialogBase';
|
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 './actions';
|
||||||
export * from './actionTypes';
|
export * from './actionTypes';
|
||||||
export * from './components';
|
export * from './components';
|
||||||
|
export * from './functions';
|
||||||
|
|
||||||
import './middleware';
|
import './middleware';
|
||||||
import './reducer';
|
import './reducer';
|
||||||
|
|
|
@ -11,8 +11,11 @@ import {
|
||||||
Transport
|
Transport
|
||||||
} from '../../../modules/transport';
|
} from '../../../modules/transport';
|
||||||
import { parseURLParams } from '../base/config';
|
import { parseURLParams } from '../base/config';
|
||||||
|
import { DeviceSelection } from '../device-selection';
|
||||||
|
|
||||||
import DeviceSelectionDialogBase from './components/DeviceSelectionDialogBase';
|
// Using the full path to the file to prevent adding unnecessary code into the
|
||||||
|
// dialog popup bundle.
|
||||||
|
import DialogWithTabs from '../base/dialog/components/DialogWithTabs';
|
||||||
|
|
||||||
const logger = Logger.getLogger(__filename);
|
const logger = Logger.getLogger(__filename);
|
||||||
|
|
||||||
|
@ -29,10 +32,9 @@ export default class DeviceSelectionPopup {
|
||||||
*/
|
*/
|
||||||
constructor(i18next) {
|
constructor(i18next) {
|
||||||
this.close = this.close.bind(this);
|
this.close = this.close.bind(this);
|
||||||
this._setVideoInputDevice = this._setVideoInputDevice.bind(this);
|
|
||||||
this._setAudioInputDevice = this._setAudioInputDevice.bind(this);
|
|
||||||
this._setAudioOutputDevice = this._setAudioOutputDevice.bind(this);
|
|
||||||
this._i18next = i18next;
|
this._i18next = i18next;
|
||||||
|
this._onSubmit = this._onSubmit.bind(this);
|
||||||
|
|
||||||
const { scope } = parseURLParams(window.location);
|
const { scope } = parseURLParams(window.location);
|
||||||
|
|
||||||
this._transport = new Transport({
|
this._transport = new Transport({
|
||||||
|
@ -56,10 +58,11 @@ export default class DeviceSelectionPopup {
|
||||||
|
|
||||||
this._dialogProps = {
|
this._dialogProps = {
|
||||||
availableDevices: {},
|
availableDevices: {},
|
||||||
currentAudioInputId: '',
|
selectedAudioInputId: '',
|
||||||
currentAudioOutputId: '',
|
selectedAudioOutputId: '',
|
||||||
currentVideoInputId: '',
|
selectedVideoInputId: '',
|
||||||
disableAudioInputChange: true,
|
disableAudioInputChange: true,
|
||||||
|
disableBlanketClickDismiss: true,
|
||||||
disableDeviceChange: true,
|
disableDeviceChange: true,
|
||||||
hasAudioPermission: JitsiMeetJS.mediaDevices
|
hasAudioPermission: JitsiMeetJS.mediaDevices
|
||||||
.isDevicePermissionGranted.bind(null, 'audio'),
|
.isDevicePermissionGranted.bind(null, 'audio'),
|
||||||
|
@ -67,6 +70,7 @@ export default class DeviceSelectionPopup {
|
||||||
.isDevicePermissionGranted.bind(null, 'video'),
|
.isDevicePermissionGranted.bind(null, 'video'),
|
||||||
hideAudioInputPreview: !JitsiMeetJS.isCollectingLocalStats(),
|
hideAudioInputPreview: !JitsiMeetJS.isCollectingLocalStats(),
|
||||||
hideAudioOutputSelect: true
|
hideAudioOutputSelect: true
|
||||||
|
|
||||||
};
|
};
|
||||||
this._initState();
|
this._initState();
|
||||||
}
|
}
|
||||||
|
@ -153,9 +157,9 @@ export default class DeviceSelectionPopup {
|
||||||
]) => {
|
]) => {
|
||||||
this._changeDialogProps({
|
this._changeDialogProps({
|
||||||
availableDevices,
|
availableDevices,
|
||||||
currentAudioInputId: currentDevices.audioInput,
|
selectedAudioInputId: currentDevices.audioInput,
|
||||||
currentAudioOutputId: currentDevices.audioOutput,
|
selectedAudioOutputId: currentDevices.audioOutput,
|
||||||
currentVideoInputId: currentDevices.videoInput,
|
selectedVideoInputId: currentDevices.videoInput,
|
||||||
disableAudioInputChange: !multiAudioInputSupported,
|
disableAudioInputChange: !multiAudioInputSupported,
|
||||||
disableDeviceChange: !listAvailable || !changeAvailable,
|
disableDeviceChange: !listAvailable || !changeAvailable,
|
||||||
hideAudioOutputSelect: !changeOutputAvailable
|
hideAudioOutputSelect: !changeOutputAvailable
|
||||||
|
@ -217,26 +221,58 @@ export default class DeviceSelectionPopup {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked to save changes to selected devices and close the
|
||||||
|
* dialog.
|
||||||
|
*
|
||||||
|
* @param {Object} newSettings - The chosen device IDs.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onSubmit(newSettings) {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
if (newSettings.selectedVideoInputId
|
||||||
|
!== this._dialogProps.selectedVideoInputId) {
|
||||||
|
promises.push(
|
||||||
|
this._setVideoInputDevice(newSettings.selectedVideoInputId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newSettings.selectedAudioInputId
|
||||||
|
!== this._dialogProps.selectedAudioInputId) {
|
||||||
|
promises.push(
|
||||||
|
this._setAudioInputDevice(newSettings.selectedAudioInputId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newSettings.selectedAudioOutputId
|
||||||
|
!== this._dialogProps.selectedAudioOutputId) {
|
||||||
|
promises.push(
|
||||||
|
this._setAudioOutputDevice(newSettings.selectedAudioOutputId));
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all(promises).then(this.close, this.close);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the React components for the popup page.
|
* Renders the React components for the popup page.
|
||||||
*
|
*
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_render() {
|
_render() {
|
||||||
const props = {
|
const onSubmit = this.close;
|
||||||
...this._dialogProps,
|
|
||||||
closeModal: this.close,
|
|
||||||
disableBlanketClickDismiss: true,
|
|
||||||
setAudioInputDevice: this._setAudioInputDevice,
|
|
||||||
setAudioOutputDevice: this._setAudioOutputDevice,
|
|
||||||
setVideoInputDevice: this._setVideoInputDevice
|
|
||||||
};
|
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<I18nextProvider
|
<I18nextProvider i18n = { this._i18next }>
|
||||||
i18n = { this._i18next }>
|
|
||||||
<AtlasKitThemeProvider mode = 'dark'>
|
<AtlasKitThemeProvider mode = 'dark'>
|
||||||
<DeviceSelectionDialogBase { ...props } />
|
<DialogWithTabs
|
||||||
|
closeDialog = { this.close }
|
||||||
|
onSubmit = { onSubmit }
|
||||||
|
tabs = { [ {
|
||||||
|
component: DeviceSelection,
|
||||||
|
label: 'settings.devices',
|
||||||
|
props: this._dialogProps,
|
||||||
|
submit: this._onSubmit
|
||||||
|
} ] } />
|
||||||
</AtlasKitThemeProvider>
|
</AtlasKitThemeProvider>
|
||||||
</I18nextProvider>,
|
</I18nextProvider>,
|
||||||
document.getElementById('react'));
|
document.getElementById('react'));
|
|
@ -1,6 +1,14 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
|
import { setFollowMe, setStartMutedPolicy } from '../base/conference';
|
||||||
|
import { openDialog } from '../base/dialog';
|
||||||
|
import { i18next } from '../base/i18n';
|
||||||
|
|
||||||
import { SET_SETTINGS_VIEW_VISIBLE } from './actionTypes';
|
import { SET_SETTINGS_VIEW_VISIBLE } from './actionTypes';
|
||||||
|
import { SettingsDialog } from './components';
|
||||||
|
import { getMoreTabProps, getProfileTabProps } from './functions';
|
||||||
|
|
||||||
|
declare var APP: Object;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the visibility of the view/UI which renders the app's settings.
|
* Sets the visibility of the view/UI which renders the app's settings.
|
||||||
|
@ -18,3 +26,61 @@ export function setSettingsViewVisible(visible: boolean) {
|
||||||
visible
|
visible
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens {@code SettingsDialog}.
|
||||||
|
*
|
||||||
|
* @param {string} defaultTab - The tab in {@code SettingsDialog} that should be
|
||||||
|
* displayed initially.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
export function openSettingsDialog(defaultTab: string) {
|
||||||
|
return openDialog(SettingsDialog, { defaultTab });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits the settings from the "More" tab of the settings dialog.
|
||||||
|
*
|
||||||
|
* @param {Object} newState - The new settings.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
export function submitMoreTab(newState: Object): Function {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const currentState = getMoreTabProps(getState());
|
||||||
|
|
||||||
|
if (newState.followMeEnabled !== currentState.followMeEnabled) {
|
||||||
|
dispatch(setFollowMe(newState.followMeEnabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newState.startAudioMuted !== currentState.startAudioMuted
|
||||||
|
|| newState.startVideoMuted !== currentState.startVideoMuted) {
|
||||||
|
dispatch(setStartMutedPolicy(
|
||||||
|
newState.startAudioMuted, newState.startVideoMuted));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newState.currentLanguage !== currentState.currentLanguage) {
|
||||||
|
i18next.changeLanguage(newState.currentLanguage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits the settings from the "Profile" tab of the settings dialog.
|
||||||
|
*
|
||||||
|
* @param {Object} newState - The new settings.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
export function submitProfileTab(newState: Object): Function {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const currentState = getProfileTabProps(getState());
|
||||||
|
|
||||||
|
if (newState.displayName !== currentState.displayName) {
|
||||||
|
APP.conference.changeLocalDisplayName(newState.displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newState.email !== currentState.email) {
|
||||||
|
APP.conference.changeLocalEmail(newState.email);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -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 { translate } from '../../../base/i18n';
|
||||||
import { AbstractButton } from '../../../base/toolbox';
|
import { AbstractButton } from '../../../base/toolbox';
|
||||||
import type { AbstractButtonProps } from '../../../base/toolbox';
|
import type { AbstractButtonProps } from '../../../base/toolbox';
|
||||||
import { openDeviceSelectionDialog } from '../../../device-selection';
|
import { openDeviceSelectionPopup } from '../../../device-selection';
|
||||||
import { toggleSettings } from '../../../side-panel';
|
|
||||||
|
import { openSettingsDialog } from '../../actions';
|
||||||
|
import { SETTINGS_TABS } from '../../constants';
|
||||||
|
|
||||||
declare var interfaceConfig: Object;
|
declare var interfaceConfig: Object;
|
||||||
|
|
||||||
|
@ -21,11 +23,6 @@ type Props = AbstractButtonProps & {
|
||||||
*/
|
*/
|
||||||
_filmstripOnly: boolean,
|
_filmstripOnly: boolean,
|
||||||
|
|
||||||
/**
|
|
||||||
* Array containing the enabled settings sections.
|
|
||||||
*/
|
|
||||||
_sections: Array<string>,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The redux {@code dispatch} function.
|
* The redux {@code dispatch} function.
|
||||||
*/
|
*/
|
||||||
|
@ -48,14 +45,13 @@ class SettingsButton extends AbstractButton<Props, *> {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_handleClick() {
|
_handleClick() {
|
||||||
const { _filmstripOnly, _sections, dispatch } = this.props;
|
const { _filmstripOnly, dispatch } = this.props;
|
||||||
|
|
||||||
sendAnalytics(createToolbarEvent('settings'));
|
sendAnalytics(createToolbarEvent('settings'));
|
||||||
if (_filmstripOnly
|
if (_filmstripOnly) {
|
||||||
|| (_sections.length === 1 && _sections.includes('devices'))) {
|
dispatch(openDeviceSelectionPopup());
|
||||||
dispatch(openDeviceSelectionDialog());
|
|
||||||
} else {
|
} else {
|
||||||
dispatch(toggleSettings());
|
dispatch(openSettingsDialog(SETTINGS_TABS.DEVICES));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,8 +71,7 @@ function _mapStateToProps(state): Object { // eslint-disable-line no-unused-vars
|
||||||
// interfaceConfig is part of redux we will.
|
// interfaceConfig is part of redux we will.
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_filmstripOnly: Boolean(interfaceConfig.filmStripOnly),
|
_filmstripOnly: Boolean(interfaceConfig.filmStripOnly)
|
||||||
_sections: interfaceConfig.SETTINGS_SECTIONS || []
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 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
|
// @flow
|
||||||
|
import { toState } from '../base/redux';
|
||||||
import { parseStandardURIString } from '../base/util';
|
import { parseStandardURIString } from '../base/util';
|
||||||
|
import { i18next, DEFAULT_LANGUAGE, LANGUAGES } from '../base/i18n';
|
||||||
|
import { getLocalParticipant, PARTICIPANT_ROLE } from '../base/participants';
|
||||||
|
|
||||||
declare var interfaceConfig: Object;
|
declare var interfaceConfig: Object;
|
||||||
|
|
||||||
|
@ -60,3 +62,57 @@ export function shouldShowOnlyDeviceSelection() {
|
||||||
return interfaceConfig.SETTINGS_SECTIONS.length === 1
|
return interfaceConfig.SETTINGS_SECTIONS.length === 1
|
||||||
&& isSettingEnabled('devices');
|
&& isSettingEnabled('devices');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the properties for the "More" tab from settings dialog from Redux
|
||||||
|
* state.
|
||||||
|
*
|
||||||
|
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
|
||||||
|
* {@code getState} function to be used to retrieve the state.
|
||||||
|
* @returns {Object} - The properties for the "More" tab from settings dialog.
|
||||||
|
*/
|
||||||
|
export function getMoreTabProps(stateful: Object | Function) {
|
||||||
|
const state = toState(stateful);
|
||||||
|
const language = i18next.language || DEFAULT_LANGUAGE;
|
||||||
|
const conference = state['features/base/conference'];
|
||||||
|
const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
|
||||||
|
const localParticipant = getLocalParticipant(state);
|
||||||
|
|
||||||
|
|
||||||
|
// The settings sections to display.
|
||||||
|
const showModeratorSettings
|
||||||
|
= configuredTabs.includes('moderator')
|
||||||
|
&& localParticipant.role === PARTICIPANT_ROLE.MODERATOR;
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentLanguage: language,
|
||||||
|
followMeEnabled: Boolean(conference.followMeEnabled),
|
||||||
|
languages: LANGUAGES,
|
||||||
|
showLanguageSettings: configuredTabs.includes('language'),
|
||||||
|
showModeratorSettings,
|
||||||
|
startAudioMuted: Boolean(conference.startAudioMutedPolicy),
|
||||||
|
startVideoMuted: Boolean(conference.startVideoMutedPolicy)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the properties for the "Profile" tab from settings dialog from Redux
|
||||||
|
* state.
|
||||||
|
*
|
||||||
|
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
|
||||||
|
* {@code getState} function to be used to retrieve the state.
|
||||||
|
* @returns {Object} - The properties for the "Profile" tab from settings
|
||||||
|
* dialog.
|
||||||
|
*/
|
||||||
|
export function getProfileTabProps(stateful: Object | Function) {
|
||||||
|
const state = toState(stateful);
|
||||||
|
const conference = state['features/base/conference'];
|
||||||
|
const localParticipant = getLocalParticipant(state);
|
||||||
|
|
||||||
|
return {
|
||||||
|
authEnabled: conference.authEnabled,
|
||||||
|
authLogin: conference.authLogin,
|
||||||
|
displayName: localParticipant.name,
|
||||||
|
email: localParticipant.email
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export * from './actions';
|
export * from './actions';
|
||||||
export * from './actionTypes';
|
export * from './actionTypes';
|
||||||
export * from './components';
|
export * from './components';
|
||||||
|
export * from './constants';
|
||||||
export * from './functions';
|
export * from './functions';
|
||||||
|
|
||||||
import './middleware';
|
import './middleware';
|
||||||
|
|
|
@ -27,23 +27,3 @@ export const SET_VISIBLE_PANEL = Symbol('SET_VISIBLE_PANEL');
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export const TOGGLE_CHAT = Symbol('TOGGLE_CHAT');
|
export const TOGGLE_CHAT = Symbol('TOGGLE_CHAT');
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of the action which signals to toggle the display of profile editing
|
|
||||||
* in the side panel.
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* type: TOGGLE_PROFILE
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
export const TOGGLE_PROFILE = Symbol('TOGGLE_PROFILE');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of the action which signals to toggle the display of settings in the
|
|
||||||
* side panel.
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* type: TOGGLE_SETTINGS
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
export const TOGGLE_SETTINGS = Symbol('TOGGLE_SETTINGS');
|
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import {
|
import {
|
||||||
CLOSE_PANEL,
|
CLOSE_PANEL,
|
||||||
SET_VISIBLE_PANEL,
|
SET_VISIBLE_PANEL,
|
||||||
TOGGLE_CHAT,
|
TOGGLE_CHAT
|
||||||
TOGGLE_PROFILE,
|
|
||||||
TOGGLE_SETTINGS
|
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,29 +47,3 @@ export function toggleChat() {
|
||||||
type: TOGGLE_CHAT
|
type: TOGGLE_CHAT
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles display of the profile side panel.
|
|
||||||
*
|
|
||||||
* @returns {{
|
|
||||||
* type: TOGGLE_PROFILE
|
|
||||||
* }}
|
|
||||||
*/
|
|
||||||
export function toggleProfile() {
|
|
||||||
return {
|
|
||||||
type: TOGGLE_PROFILE
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles display of the settings side panel.
|
|
||||||
*
|
|
||||||
* @returns {{
|
|
||||||
* type: TOGGLE_SETTINGS
|
|
||||||
* }}
|
|
||||||
*/
|
|
||||||
export function toggleSettings() {
|
|
||||||
return {
|
|
||||||
type: TOGGLE_SETTINGS
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,12 +2,7 @@
|
||||||
|
|
||||||
import { MiddlewareRegistry } from '../base/redux';
|
import { MiddlewareRegistry } from '../base/redux';
|
||||||
|
|
||||||
import {
|
import { CLOSE_PANEL, TOGGLE_CHAT } from './actionTypes';
|
||||||
CLOSE_PANEL,
|
|
||||||
TOGGLE_CHAT,
|
|
||||||
TOGGLE_PROFILE,
|
|
||||||
TOGGLE_SETTINGS
|
|
||||||
} from './actionTypes';
|
|
||||||
|
|
||||||
declare var APP: Object;
|
declare var APP: Object;
|
||||||
|
|
||||||
|
@ -31,14 +26,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
case TOGGLE_CHAT:
|
case TOGGLE_CHAT:
|
||||||
APP.UI.toggleChat();
|
APP.UI.toggleChat();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TOGGLE_PROFILE:
|
|
||||||
APP.UI.toggleSidePanel('profile_container');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case TOGGLE_SETTINGS:
|
|
||||||
APP.UI.toggleSidePanel('settings_container');
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return next(action);
|
return next(action);
|
||||||
|
|
|
@ -114,6 +114,7 @@ function _mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
_localParticipant: getLocalParticipant(state),
|
_localParticipant: getLocalParticipant(state),
|
||||||
_unclickable: !state['features/base/jwt'].isGuest
|
_unclickable: !state['features/base/jwt'].isGuest
|
||||||
|
|| !interfaceConfig.SETTINGS_SECTIONS.includes('profile')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,9 +36,13 @@ import {
|
||||||
StopRecordingDialog,
|
StopRecordingDialog,
|
||||||
getActiveSession
|
getActiveSession
|
||||||
} from '../../../recording';
|
} from '../../../recording';
|
||||||
import { SettingsButton } from '../../../settings';
|
import {
|
||||||
|
SETTINGS_TABS,
|
||||||
|
SettingsButton,
|
||||||
|
openSettingsDialog
|
||||||
|
} from '../../../settings';
|
||||||
import { toggleSharedVideo } from '../../../shared-video';
|
import { toggleSharedVideo } from '../../../shared-video';
|
||||||
import { toggleChat, toggleProfile } from '../../../side-panel';
|
import { toggleChat } from '../../../side-panel';
|
||||||
import { SpeakerStats } from '../../../speaker-stats';
|
import { SpeakerStats } from '../../../speaker-stats';
|
||||||
import {
|
import {
|
||||||
OverflowMenuVideoQualityItem,
|
OverflowMenuVideoQualityItem,
|
||||||
|
@ -515,7 +519,7 @@ class Toolbox extends Component<Props> {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_doToggleProfile() {
|
_doToggleProfile() {
|
||||||
this.props.dispatch(toggleProfile());
|
this.props.dispatch(openSettingsDialog(SETTINGS_TABS.PROFILE));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -36,12 +36,6 @@ export default {
|
||||||
*/
|
*/
|
||||||
TOGGLE_AUDIO_ONLY: 'UI.toggle_audioonly',
|
TOGGLE_AUDIO_ONLY: 'UI.toggle_audioonly',
|
||||||
TOGGLE_CHAT: 'UI.toggle_chat',
|
TOGGLE_CHAT: 'UI.toggle_chat',
|
||||||
TOGGLE_SETTINGS: 'UI.toggle_settings',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies that the profile toolbar button has been clicked.
|
|
||||||
*/
|
|
||||||
TOGGLE_PROFILE: 'UI.toggle_profile',
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies that a command to toggle the filmstrip has been issued. The
|
* Notifies that a command to toggle the filmstrip has been issued. The
|
||||||
|
|
|
@ -146,7 +146,7 @@ module.exports = [
|
||||||
],
|
],
|
||||||
|
|
||||||
'device_selection_popup_bundle':
|
'device_selection_popup_bundle':
|
||||||
'./react/features/device-selection/popup.js',
|
'./react/features/settings/popup.js',
|
||||||
|
|
||||||
'alwaysontop':
|
'alwaysontop':
|
||||||
'./react/features/always-on-top/index.js',
|
'./react/features/always-on-top/index.js',
|
||||||
|
|
Loading…
Reference in New Issue