feat(device_selection): Implement popup
This commit is contained in:
parent
2c002c875d
commit
96e83989a5
2
Makefile
2
Makefile
|
@ -31,6 +31,8 @@ deploy-appbundle:
|
||||||
$(BUILD_DIR)/do_external_connect.min.map \
|
$(BUILD_DIR)/do_external_connect.min.map \
|
||||||
$(BUILD_DIR)/external_api.min.js \
|
$(BUILD_DIR)/external_api.min.js \
|
||||||
$(BUILD_DIR)/external_api.min.map \
|
$(BUILD_DIR)/external_api.min.map \
|
||||||
|
$(BUILD_DIR)/device_selection_popup_bundle.min.js \
|
||||||
|
$(BUILD_DIR)/device_selection_popup_bundle.min.map \
|
||||||
$(OUTPUT_DIR)/analytics.js \
|
$(OUTPUT_DIR)/analytics.js \
|
||||||
$(DEPLOY_DIR)
|
$(DEPLOY_DIR)
|
||||||
|
|
||||||
|
|
|
@ -136,7 +136,7 @@
|
||||||
&__videos-filmstripOnly {
|
&__videos-filmstripOnly {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
padding-right: $defaultToolbarSize;
|
padding-right: $defaultFilmStripOnlyToolbarSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remote-videos-container {
|
.remote-videos-container {
|
||||||
|
|
|
@ -200,7 +200,13 @@
|
||||||
height: auto;
|
height: auto;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: $defaultToolbarSize;
|
width: $defaultFilmStripOnlyToolbarSize;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
height: 37px;
|
||||||
|
line-height: 37px !important;
|
||||||
|
width: 37px;
|
||||||
|
}
|
||||||
|
|
||||||
.button:first-child {
|
.button:first-child {
|
||||||
border-top-left-radius: 3px;
|
border-top-left-radius: 3px;
|
||||||
|
|
|
@ -34,6 +34,7 @@ $tooltipBg: rgba(0,0,0, 0.7);
|
||||||
* Toolbar
|
* Toolbar
|
||||||
*/
|
*/
|
||||||
$defaultToolbarSize: 50px;
|
$defaultToolbarSize: 50px;
|
||||||
|
$defaultFilmStripOnlyToolbarSize: 37px;
|
||||||
$splitterToolbarButtonMargin: 18px;
|
$splitterToolbarButtonMargin: 18px;
|
||||||
$toolbarBackground: rgba(0, 0, 0, 0.5);
|
$toolbarBackground: rgba(0, 0, 0, 0.5);
|
||||||
$toolbarBadgeBackground: #165ECC;
|
$toolbarBadgeBackground: #165ECC;
|
||||||
|
|
|
@ -36,14 +36,14 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars
|
||||||
*/
|
*/
|
||||||
TOOLBAR_BUTTONS: [
|
TOOLBAR_BUTTONS: [
|
||||||
//main toolbar
|
//main toolbar
|
||||||
'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup',
|
'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup', 'deviceselection', // jshint ignore:line
|
||||||
//extended toolbar
|
//extended toolbar
|
||||||
'profile', 'contacts', 'chat', 'recording', 'etherpad', 'sharedvideo', 'dialout', 'settings', 'raisehand', 'filmstrip'], // jshint ignore:line
|
'profile', 'contacts', 'chat', 'recording', 'etherpad', 'sharedvideo', 'dialout', 'settings', 'raisehand', 'filmstrip'], // jshint ignore:line
|
||||||
/**
|
/**
|
||||||
* Main Toolbar Buttons
|
* Main Toolbar Buttons
|
||||||
* All of them should be in TOOLBAR_BUTTONS
|
* All of them should be in TOOLBAR_BUTTONS
|
||||||
*/
|
*/
|
||||||
MAIN_TOOLBAR_BUTTONS: ['microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup'], // jshint ignore:line
|
MAIN_TOOLBAR_BUTTONS: ['microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup', 'deviceselection'], // jshint ignore:line
|
||||||
SETTINGS_SECTIONS: ['language', 'devices', 'moderator'],
|
SETTINGS_SECTIONS: ['language', 'devices', 'moderator'],
|
||||||
// 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
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import { hideDialog } from '../actions';
|
import { hideDialog } from '../actions';
|
||||||
|
import { dialogPropTypes } from '../constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract dialog to display dialogs.
|
* Abstract dialog to display dialogs.
|
||||||
|
@ -13,57 +14,12 @@ export default class AbstractDialog extends Component {
|
||||||
* @static
|
* @static
|
||||||
*/
|
*/
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
/**
|
...dialogPropTypes,
|
||||||
* Whether cancel button is disabled. Enabled by default.
|
|
||||||
*/
|
|
||||||
cancelDisabled: React.PropTypes.bool,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional i18n key to change the cancel button title.
|
|
||||||
*/
|
|
||||||
cancelTitleKey: React.PropTypes.string,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to show/hide the dialog on cancel.
|
* Used to show/hide the dialog on cancel.
|
||||||
*/
|
*/
|
||||||
dispatch: React.PropTypes.func,
|
dispatch: React.PropTypes.func
|
||||||
|
|
||||||
/**
|
|
||||||
* Is ok button enabled/disabled. Enabled by default.
|
|
||||||
*/
|
|
||||||
okDisabled: React.PropTypes.bool,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional i18n key to change the ok button title.
|
|
||||||
*/
|
|
||||||
okTitleKey: React.PropTypes.string,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The handler for onCancel event.
|
|
||||||
*/
|
|
||||||
onCancel: React.PropTypes.func,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The handler for the event when submitting the dialog.
|
|
||||||
*/
|
|
||||||
onSubmit: React.PropTypes.func,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to obtain translations in children classes.
|
|
||||||
*/
|
|
||||||
t: React.PropTypes.func,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Key to use for showing a title.
|
|
||||||
*/
|
|
||||||
titleKey: React.PropTypes.string,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The string to use as a title instead of {@code titleKey}. If a truthy
|
|
||||||
* value is specified, it takes precedence over {@code titleKey} i.e.
|
|
||||||
* the latter is unused.
|
|
||||||
*/
|
|
||||||
titleString: React.PropTypes.string
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
import AKButton from '@atlaskit/button';
|
|
||||||
import AKButtonGroup from '@atlaskit/button-group';
|
|
||||||
import ModalDialog from '@atlaskit/modal-dialog';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { translate } from '../../i18n';
|
|
||||||
|
|
||||||
import AbstractDialog from './AbstractDialog';
|
import AbstractDialog from './AbstractDialog';
|
||||||
|
import StatelessDialog from './StatelessDialog';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Web dialog that uses atlaskit modal-dialog to display dialogs.
|
* Web dialog that uses atlaskit modal-dialog to display dialogs.
|
||||||
|
@ -19,6 +15,8 @@ class Dialog extends AbstractDialog {
|
||||||
* @static
|
* @static
|
||||||
*/
|
*/
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
...AbstractDialog.propTypes,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the body of the dialog, the component children.
|
* This is the body of the dialog, the component children.
|
||||||
*/
|
*/
|
||||||
|
@ -30,6 +28,11 @@ class Dialog extends AbstractDialog {
|
||||||
*/
|
*/
|
||||||
isModal: React.PropTypes.bool,
|
isModal: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables rendering of the submit button.
|
||||||
|
*/
|
||||||
|
submitDisabled: React.PropTypes.bool,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Width of the dialog, can be:
|
* Width of the dialog, can be:
|
||||||
* - 'small' (400px), 'medium' (600px), 'large' (800px),
|
* - 'small' (400px), 'medium' (600px), 'large' (800px),
|
||||||
|
@ -40,6 +43,19 @@ class Dialog extends AbstractDialog {
|
||||||
width: React.PropTypes.string
|
width: React.PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new Dialog instance.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The read-only properties with which the new
|
||||||
|
* instance is to be initialized.
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._onCancel = this._onCancel.bind(this);
|
||||||
|
this._onSubmit = this._onSubmit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -47,101 +63,15 @@ class Dialog extends AbstractDialog {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
return (
|
const props = {
|
||||||
<ModalDialog
|
...this.props,
|
||||||
footer = { this._renderFooter() }
|
onSubmit: this._onSubmit,
|
||||||
header = { this._renderHeader() }
|
onCancel: this._onCancel
|
||||||
isOpen = { true }
|
};
|
||||||
onDialogDismissed = { this._onCancel }
|
|
||||||
width = { this.props.width || 'medium' }>
|
|
||||||
<div>
|
|
||||||
<form
|
|
||||||
className = 'modal-dialog-form'
|
|
||||||
id = 'modal-dialog-form'
|
|
||||||
onSubmit = { this._onSubmit }>
|
|
||||||
{ this.props.children }
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</ModalDialog>);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
delete props.dispatch;
|
||||||
* Render cancel button.
|
|
||||||
*
|
|
||||||
* @returns {*} The cancel button if enabled and dialog is not modal.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_renderCancelButton() {
|
|
||||||
if (this.props.cancelDisabled || this.props.isModal) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return <StatelessDialog { ...props } />;
|
||||||
<AKButton
|
|
||||||
appearance = 'subtle'
|
|
||||||
id = 'modal-dialog-cancel-button'
|
|
||||||
onClick = { this._onCancel }>
|
|
||||||
{ this.props.t(this.props.cancelTitleKey || 'dialog.Cancel') }
|
|
||||||
</AKButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render component in dialog footer.
|
|
||||||
*
|
|
||||||
* @returns {ReactElement}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_renderFooter() {
|
|
||||||
return (
|
|
||||||
<footer className = 'modal-dialog-footer'>
|
|
||||||
<AKButtonGroup>
|
|
||||||
{ this._renderCancelButton() }
|
|
||||||
{ this._renderOKButton() }
|
|
||||||
</AKButtonGroup>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render component in dialog header.
|
|
||||||
*
|
|
||||||
* @returns {ReactElement}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_renderHeader() {
|
|
||||||
const { t } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header>
|
|
||||||
<h2>
|
|
||||||
{ this.props.titleString || t(this.props.titleKey) }
|
|
||||||
</h2>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render ok button.
|
|
||||||
*
|
|
||||||
* @returns {*} The ok button if enabled.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_renderOKButton() {
|
|
||||||
if (this.props.submitDisabled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AKButton
|
|
||||||
appearance = 'primary'
|
|
||||||
form = 'modal-dialog-form'
|
|
||||||
id = 'modal-dialog-ok-button'
|
|
||||||
isDisabled = { this.props.okDisabled }
|
|
||||||
onClick = { this._onSubmit }>
|
|
||||||
{ this.props.t(this.props.okTitleKey || 'dialog.Ok') }
|
|
||||||
</AKButton>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -158,4 +88,4 @@ class Dialog extends AbstractDialog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate(connect()(Dialog));
|
export default connect()(Dialog);
|
||||||
|
|
|
@ -0,0 +1,210 @@
|
||||||
|
import AKButton from '@atlaskit/button';
|
||||||
|
import AKButtonGroup from '@atlaskit/button-group';
|
||||||
|
import ModalDialog from '@atlaskit/modal-dialog';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { translate } from '../../i18n';
|
||||||
|
|
||||||
|
import { dialogPropTypes } from '../constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web dialog that uses atlaskit modal-dialog to display dialogs.
|
||||||
|
*/
|
||||||
|
class StatelessDialog extends Component {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web dialog component's property types.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
static propTypes = {
|
||||||
|
...dialogPropTypes,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the body of the dialog, the component children.
|
||||||
|
*/
|
||||||
|
children: React.PropTypes.node,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables dismissing the dialog when the blanket is clicked. Enabled
|
||||||
|
* by default.
|
||||||
|
*/
|
||||||
|
disableBlanketClickDismiss: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the dialog is modal. This means clicking on the blanket will
|
||||||
|
* leave the dialog open. No cancel button.
|
||||||
|
*/
|
||||||
|
isModal: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables rendering of the submit button.
|
||||||
|
*/
|
||||||
|
submitDisabled: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Width of the dialog, can be:
|
||||||
|
* - 'small' (400px), 'medium' (600px), 'large' (800px),
|
||||||
|
* 'x-large' (968px)
|
||||||
|
* - integer value for pixel width
|
||||||
|
* - string value for percentage
|
||||||
|
*/
|
||||||
|
width: React.PropTypes.string
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new Dialog instance.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The read-only properties with which the new
|
||||||
|
* instance is to be initialized.
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._onCancel = this._onCancel.bind(this);
|
||||||
|
this._onDialogDismissed = this._onDialogDismissed.bind(this);
|
||||||
|
this._onSubmit = this._onSubmit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<ModalDialog
|
||||||
|
footer = { this._renderFooter() }
|
||||||
|
header = { this._renderHeader() }
|
||||||
|
isOpen = { true }
|
||||||
|
onDialogDismissed = { this._onDialogDismissed }
|
||||||
|
width = { this.props.width || 'medium' }>
|
||||||
|
<div>
|
||||||
|
<form
|
||||||
|
className = 'modal-dialog-form'
|
||||||
|
id = 'modal-dialog-form'
|
||||||
|
onSubmit = { this._onSubmit }>
|
||||||
|
{ this.props.children }
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</ModalDialog>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles click on the blanket area.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onDialogDismissed() {
|
||||||
|
if (!this.props.disableBlanketClickDismiss) {
|
||||||
|
this._onCancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render cancel button.
|
||||||
|
*
|
||||||
|
* @returns {*} The cancel button if enabled and dialog is not modal.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_renderCancelButton() {
|
||||||
|
if (this.props.cancelDisabled || this.props.isModal) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AKButton
|
||||||
|
appearance = 'subtle'
|
||||||
|
id = 'modal-dialog-cancel-button'
|
||||||
|
onClick = { this._onCancel }>
|
||||||
|
{ this.props.t(this.props.cancelTitleKey || 'dialog.Cancel') }
|
||||||
|
</AKButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render component in dialog footer.
|
||||||
|
*
|
||||||
|
* @returns {ReactElement}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_renderFooter() {
|
||||||
|
return (
|
||||||
|
<footer className = 'modal-dialog-footer'>
|
||||||
|
<AKButtonGroup>
|
||||||
|
{ this._renderCancelButton() }
|
||||||
|
{ this._renderOKButton() }
|
||||||
|
</AKButtonGroup>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render component in dialog header.
|
||||||
|
*
|
||||||
|
* @returns {ReactElement}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_renderHeader() {
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header>
|
||||||
|
<h2>
|
||||||
|
{ this.props.titleString || t(this.props.titleKey) }
|
||||||
|
</h2>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render ok button.
|
||||||
|
*
|
||||||
|
* @returns {*} The ok button if enabled.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_renderOKButton() {
|
||||||
|
if (this.props.submitDisabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AKButton
|
||||||
|
appearance = 'primary'
|
||||||
|
form = 'modal-dialog-form'
|
||||||
|
id = 'modal-dialog-ok-button'
|
||||||
|
isDisabled = { this.props.okDisabled }
|
||||||
|
onClick = { this._onSubmit }>
|
||||||
|
{ this.props.t(this.props.okTitleKey || 'dialog.Ok') }
|
||||||
|
</AKButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches action to hide the dialog.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onCancel() {
|
||||||
|
if (this.props.isModal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches the action when submitting the dialog.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {string} value - The submitted value if any.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onSubmit(value) {
|
||||||
|
this.props.onSubmit(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(StatelessDialog);
|
|
@ -1,2 +1,3 @@
|
||||||
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';
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const dialogPropTypes = {
|
||||||
|
/**
|
||||||
|
* Whether cancel button is disabled. Enabled by default.
|
||||||
|
*/
|
||||||
|
cancelDisabled: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional i18n key to change the cancel button title.
|
||||||
|
*/
|
||||||
|
cancelTitleKey: React.PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is ok button enabled/disabled. Enabled by default.
|
||||||
|
*/
|
||||||
|
okDisabled: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional i18n key to change the ok button title.
|
||||||
|
*/
|
||||||
|
okTitleKey: React.PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The handler for onCancel event.
|
||||||
|
*/
|
||||||
|
onCancel: React.PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The handler for the event when submitting the dialog.
|
||||||
|
*/
|
||||||
|
onSubmit: React.PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to obtain translations in children classes.
|
||||||
|
*/
|
||||||
|
t: React.PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key to use for showing a title.
|
||||||
|
*/
|
||||||
|
titleKey: React.PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The string to use as a title instead of {@code titleKey}. If a truthy
|
||||||
|
* value is specified, it takes precedence over {@code titleKey} i.e.
|
||||||
|
* the latter is unused.
|
||||||
|
*/
|
||||||
|
titleString: React.PropTypes.string
|
||||||
|
};
|
|
@ -0,0 +1,300 @@
|
||||||
|
import Logger from 'jitsi-meet-logger';
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { I18nextProvider } from 'react-i18next';
|
||||||
|
|
||||||
|
import {
|
||||||
|
PostMessageTransportBackend,
|
||||||
|
Transport
|
||||||
|
} from '../../../modules/transport';
|
||||||
|
import { parseURLParams } from '../base/config';
|
||||||
|
|
||||||
|
import DeviceSelectionDialogBase from './components/DeviceSelectionDialogBase';
|
||||||
|
|
||||||
|
declare var JitsiMeetJS: Object;
|
||||||
|
|
||||||
|
const logger = Logger.getLogger(__filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a class that renders the React components for the device selection
|
||||||
|
* popup page and handles the communication between the components and Jitsi
|
||||||
|
* Meet.
|
||||||
|
*/
|
||||||
|
export default class DeviceSelectionPopup {
|
||||||
|
/**
|
||||||
|
* Initializes a new DeviceSelectionPopup instance.
|
||||||
|
*
|
||||||
|
* @param {Object} i18next - The i18next instance used for translation.
|
||||||
|
*/
|
||||||
|
constructor(i18next) {
|
||||||
|
this.close = this.close.bind(this);
|
||||||
|
this._setVideoInputDevice = this._setVideoInputDevice.bind(this);
|
||||||
|
this._setAudioInputDevice = this._setAudioInputDevice.bind(this);
|
||||||
|
this._setAudioOutputDevice = this._setAudioOutputDevice.bind(this);
|
||||||
|
this._i18next = i18next;
|
||||||
|
const { scope } = parseURLParams(window.location);
|
||||||
|
|
||||||
|
this._transport = new Transport({
|
||||||
|
backend: new PostMessageTransportBackend({
|
||||||
|
postisOptions: {
|
||||||
|
scope,
|
||||||
|
window: window.opener
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
this._transport.on('event', event => {
|
||||||
|
if (event.name === 'deviceListChanged') {
|
||||||
|
this._updateAvailableDevices();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this._dialogProps = {
|
||||||
|
availableDevices: {},
|
||||||
|
currentAudioInputId: '',
|
||||||
|
currentAudioOutputId: '',
|
||||||
|
currentVideoInputId: '',
|
||||||
|
disableAudioInputChange: true,
|
||||||
|
disableDeviceChange: true,
|
||||||
|
hasAudioPermission: JitsiMeetJS.mediaDevices
|
||||||
|
.isDevicePermissionGranted('audio'),
|
||||||
|
hasVideoPermission: JitsiMeetJS.mediaDevices
|
||||||
|
.isDevicePermissionGranted('video'),
|
||||||
|
hideAudioInputPreview: !JitsiMeetJS.isCollectingLocalStats(),
|
||||||
|
hideAudioOutputSelect: true
|
||||||
|
};
|
||||||
|
this._initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends event to Jitsi Meet to close the popup dialog.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
this._transport.sendEvent({
|
||||||
|
type: 'devices-dialog',
|
||||||
|
name: 'close'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the properties of the react component and re-renders it.
|
||||||
|
*
|
||||||
|
* @param {Object} newProps - The new properties that will be assigned to
|
||||||
|
* the current ones.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_changeDialogProps(newProps) {
|
||||||
|
this._dialogProps = {
|
||||||
|
...this._dialogProps,
|
||||||
|
...newProps
|
||||||
|
};
|
||||||
|
this._render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Promise that resolves with result an list of available devices.
|
||||||
|
*
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
_getAvailableDevices() {
|
||||||
|
return this._transport.sendRequest({
|
||||||
|
type: 'devices',
|
||||||
|
name: 'getAvailableDevices'
|
||||||
|
}).catch(e => {
|
||||||
|
logger.error(e);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Promise that resolves with current selected devices.
|
||||||
|
*
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
_getCurrentDevices() {
|
||||||
|
return this._transport.sendRequest({
|
||||||
|
type: 'devices',
|
||||||
|
name: 'getCurrentDevices'
|
||||||
|
}).catch(e => {
|
||||||
|
logger.error(e);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the state.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_initState() {
|
||||||
|
return Promise.all([
|
||||||
|
this._getAvailableDevices(),
|
||||||
|
this._isDeviceListAvailable(),
|
||||||
|
this._isDeviceChangeAvailable(),
|
||||||
|
this._getCurrentDevices(),
|
||||||
|
this._isMultipleAudioInputSupported()
|
||||||
|
]).then(([
|
||||||
|
availableDevices,
|
||||||
|
listAvailable,
|
||||||
|
changeAvailable,
|
||||||
|
currentDevices,
|
||||||
|
multiAudioInputSupported
|
||||||
|
]) => {
|
||||||
|
this._changeDialogProps({
|
||||||
|
availableDevices,
|
||||||
|
currentAudioInputId: currentDevices.audioInput,
|
||||||
|
currentAudioOutputId: currentDevices.audioOutput,
|
||||||
|
currentVideoInputId: currentDevices.videoInput,
|
||||||
|
disableAudioInputChange: !multiAudioInputSupported,
|
||||||
|
disableDeviceChange: !listAvailable || !changeAvailable,
|
||||||
|
hideAudioOutputSelect: !changeAvailable
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Promise that resolves with true if the device change is available
|
||||||
|
* and with false if not.
|
||||||
|
*
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
_isDeviceChangeAvailable() {
|
||||||
|
return this._transport.sendRequest({
|
||||||
|
type: 'devices',
|
||||||
|
name: 'isDeviceChangeAvailable'
|
||||||
|
}).catch(e => {
|
||||||
|
logger.error(e);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Promise that resolves with true if the device list is available
|
||||||
|
* and with false if not.
|
||||||
|
*
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
_isDeviceListAvailable() {
|
||||||
|
return this._transport.sendRequest({
|
||||||
|
type: 'devices',
|
||||||
|
name: 'isDeviceListAvailable'
|
||||||
|
}).catch(e => {
|
||||||
|
logger.error(e);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Promise that resolves with true if the device list is available
|
||||||
|
* and with false if not.
|
||||||
|
*
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
_isMultipleAudioInputSupported() {
|
||||||
|
return this._transport.sendRequest({
|
||||||
|
type: 'devices',
|
||||||
|
name: 'isMultipleAudioInputSupported'
|
||||||
|
}).catch(e => {
|
||||||
|
logger.error(e);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the React components for the popup page.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_render() {
|
||||||
|
const props = {
|
||||||
|
...this._dialogProps,
|
||||||
|
closeModal: this.close,
|
||||||
|
disableBlanketClickDismiss: true,
|
||||||
|
setAudioInputDevice: this._setAudioInputDevice,
|
||||||
|
setAudioOutputDevice: this._setAudioOutputDevice,
|
||||||
|
setVideoInputDevice: this._setVideoInputDevice
|
||||||
|
};
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<I18nextProvider
|
||||||
|
i18n = { this._i18next }>
|
||||||
|
<DeviceSelectionDialogBase { ...props } />
|
||||||
|
</I18nextProvider>,
|
||||||
|
document.getElementById('react'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the audio input device to the one with the id that is passed.
|
||||||
|
*
|
||||||
|
* @param {string} id - The id of the new device.
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
_setAudioInputDevice(id) {
|
||||||
|
return this._setDevice({
|
||||||
|
id,
|
||||||
|
kind: 'audioinput'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the audio output device to the one with the id that is passed.
|
||||||
|
*
|
||||||
|
* @param {string} id - The id of the new device.
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
_setAudioOutputDevice(id) {
|
||||||
|
return this._setDevice({
|
||||||
|
id,
|
||||||
|
kind: 'audiooutput'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the currently used device to the one that is passed.
|
||||||
|
*
|
||||||
|
* @param {Object} device - The new device to be used.
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
_setDevice(device) {
|
||||||
|
return this._transport.sendRequest({
|
||||||
|
type: 'devices',
|
||||||
|
name: 'setDevice',
|
||||||
|
device
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the video input device to the one with the id that is passed.
|
||||||
|
*
|
||||||
|
* @param {string} id - The id of the new device.
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
_setVideoInputDevice(id) {
|
||||||
|
return this._setDevice({
|
||||||
|
id,
|
||||||
|
kind: 'videoinput'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the available devices.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_updateAvailableDevices() {
|
||||||
|
this._getAvailableDevices().then(devices =>
|
||||||
|
this._changeDialogProps({ availableDevices: devices })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
/**
|
||||||
|
* The type of Redux action which Sets information about device selection popup.
|
||||||
|
*
|
||||||
|
* {{
|
||||||
|
* type: SET_DEVICE_SELECTION_POPUP_DATA,
|
||||||
|
* popupDialogData: Object
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export const SET_DEVICE_SELECTION_POPUP_DATA
|
||||||
|
= Symbol('SET_DEVICE_SELECTION_POPUP_DATA');
|
|
@ -1,8 +1,20 @@
|
||||||
/* globals APP */
|
/* globals APP, interfaceConfig */
|
||||||
|
|
||||||
import { openDialog } from '../base/dialog';
|
import { openDialog } from '../base/dialog';
|
||||||
import JitsiMeetJS from '../base/lib-jitsi-meet';
|
import JitsiMeetJS from '../base/lib-jitsi-meet';
|
||||||
|
import { API_ID } from '../../../modules/API/constants';
|
||||||
|
import {
|
||||||
|
setAudioInputDevice,
|
||||||
|
setAudioOutputDevice,
|
||||||
|
setVideoInputDevice
|
||||||
|
} from '../base/devices';
|
||||||
|
import { i18next } from '../base/i18n';
|
||||||
|
import {
|
||||||
|
PostMessageTransportBackend,
|
||||||
|
Transport
|
||||||
|
} from '../../../modules/transport';
|
||||||
|
|
||||||
|
import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
|
||||||
import { DeviceSelectionDialog } from './components';
|
import { DeviceSelectionDialog } from './components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,6 +25,21 @@ import { DeviceSelectionDialog } from './components';
|
||||||
*/
|
*/
|
||||||
export function openDeviceSelectionDialog() {
|
export function openDeviceSelectionDialog() {
|
||||||
return dispatch => {
|
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()
|
JitsiMeetJS.mediaDevices.isDeviceListAvailable()
|
||||||
.then(isDeviceListAvailable => {
|
.then(isDeviceListAvailable => {
|
||||||
dispatch(openDialog(DeviceSelectionDialog, {
|
dispatch(openDialog(DeviceSelectionDialog, {
|
||||||
|
@ -33,5 +60,152 @@ export function openDeviceSelectionDialog() {
|
||||||
.isDeviceChangeAvailable('output')
|
.isDeviceChangeAvailable('output')
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a popup window with the device selection dialog in it.
|
||||||
|
*
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
function _openDeviceSelectionDialogInPopup() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const { popupDialogData } = getState()['features/device-selection'];
|
||||||
|
|
||||||
|
if (popupDialogData) {
|
||||||
|
popupDialogData.popup.focus();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API_ID will always be defined because the iframe api is enabled
|
||||||
|
const scope = `dialog_${API_ID}`;
|
||||||
|
const url = `static/deviceSelectionPopup.html#scope=${
|
||||||
|
encodeURIComponent(JSON.stringify(scope))}`;
|
||||||
|
const popup
|
||||||
|
= window.open(
|
||||||
|
url,
|
||||||
|
'device-selection-popup',
|
||||||
|
'toolbar=no,scrollbars=no,resizable=no,width=720,height=458');
|
||||||
|
|
||||||
|
popup.addEventListener('DOMContentLoaded', () => {
|
||||||
|
popup.init(i18next);
|
||||||
|
});
|
||||||
|
|
||||||
|
const transport = new Transport({
|
||||||
|
backend: new PostMessageTransportBackend({
|
||||||
|
postisOptions: {
|
||||||
|
scope,
|
||||||
|
window: popup
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
transport.on('request',
|
||||||
|
_processRequest.bind(undefined, dispatch, getState));
|
||||||
|
transport.on('event', event => {
|
||||||
|
if (event.type === 'devices-dialog' && event.name === 'close') {
|
||||||
|
popup.close();
|
||||||
|
transport.dispose();
|
||||||
|
dispatch(_setDeviceSelectionPopupData());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(_setDeviceSelectionPopupData({
|
||||||
|
popup,
|
||||||
|
transport
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes device requests from external applications.
|
||||||
|
*
|
||||||
|
* @param {Dispatch} dispatch - The redux {@code dispatch} function.
|
||||||
|
* @param {Function} getState - The redux function that gets/retrieves the redux
|
||||||
|
* state.
|
||||||
|
* @param {Object} request - The request to be processed.
|
||||||
|
* @param {Function} responseCallback - The callback that will send the
|
||||||
|
* response.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/ // eslint-disable-next-line max-params
|
||||||
|
function _processRequest(dispatch, getState, request, responseCallback) {
|
||||||
|
if (request.type === 'devices') {
|
||||||
|
switch (request.name) {
|
||||||
|
case 'isDeviceListAvailable':
|
||||||
|
JitsiMeetJS.mediaDevices.isDeviceListAvailable()
|
||||||
|
.then(isDeviceListAvailable =>
|
||||||
|
responseCallback(isDeviceListAvailable))
|
||||||
|
.catch(e => responseCallback(null, e));
|
||||||
|
break;
|
||||||
|
case 'isDeviceChangeAvailable':
|
||||||
|
responseCallback(
|
||||||
|
JitsiMeetJS.mediaDevices.isDeviceChangeAvailable());
|
||||||
|
break;
|
||||||
|
case 'isMultipleAudioInputSupported':
|
||||||
|
responseCallback(JitsiMeetJS.isMultipleAudioInputSupported());
|
||||||
|
break;
|
||||||
|
case 'getCurrentDevices':
|
||||||
|
responseCallback({
|
||||||
|
audioInput: APP.settings.getMicDeviceId(),
|
||||||
|
audioOutput: APP.settings.getAudioOutputDeviceId(),
|
||||||
|
videoInput: APP.settings.getCameraDeviceId()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'getAvailableDevices':
|
||||||
|
responseCallback(getState()['features/base/devices']);
|
||||||
|
break;
|
||||||
|
case 'setDevice': {
|
||||||
|
let action;
|
||||||
|
const { device } = request;
|
||||||
|
|
||||||
|
switch (device.kind) {
|
||||||
|
case 'audioinput':
|
||||||
|
action = setAudioInputDevice;
|
||||||
|
break;
|
||||||
|
case 'audiooutput':
|
||||||
|
action = setAudioOutputDevice;
|
||||||
|
break;
|
||||||
|
case 'videoinput':
|
||||||
|
action = setVideoInputDevice;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
|
||||||
|
}
|
||||||
|
dispatch(action(device.id));
|
||||||
|
responseCallback(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets information about device selection popup in the store.
|
||||||
|
*
|
||||||
|
* @param {Object} popupDialogData - Information about the popup.
|
||||||
|
* @param {Object} popupDialog.popup - The popup object returned from
|
||||||
|
* window.open.
|
||||||
|
* @param {Object} popupDialogData.transport - The transport instance used for
|
||||||
|
* communication with the popup window.
|
||||||
|
* @returns {{
|
||||||
|
* type: SET_DEVICE_SELECTION_POPUP_DATA,
|
||||||
|
* popupDialogData: Object
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
function _setDeviceSelectionPopupData(popupDialogData) {
|
||||||
|
return {
|
||||||
|
type: SET_DEVICE_SELECTION_POPUP_DATA,
|
||||||
|
popupDialogData
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ class AudioInputPreview extends PureComponent {
|
||||||
*/
|
*/
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
this._listenForAudioUpdates(nextProps.track);
|
this._listenForAudioUpdates(nextProps.track);
|
||||||
this._updateAudioLevel(0);
|
this._updateAudioLevel(undefined, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,17 +6,9 @@ import {
|
||||||
setAudioOutputDevice,
|
setAudioOutputDevice,
|
||||||
setVideoInputDevice
|
setVideoInputDevice
|
||||||
} from '../../base/devices';
|
} from '../../base/devices';
|
||||||
import {
|
import { hideDialog } from '../../base/dialog';
|
||||||
Dialog,
|
|
||||||
hideDialog
|
|
||||||
} from '../../base/dialog';
|
|
||||||
import { translate } from '../../base/i18n';
|
|
||||||
import { createLocalTrack } from '../../base/lib-jitsi-meet';
|
|
||||||
|
|
||||||
import AudioInputPreview from './AudioInputPreview';
|
import DeviceSelectionDialogBase from './DeviceSelectionDialogBase';
|
||||||
import AudioOutputPreview from './AudioOutputPreview';
|
|
||||||
import DeviceSelector from './DeviceSelector';
|
|
||||||
import VideoInputPreview from './VideoInputPreview';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React component for previewing and selecting new audio and video sources.
|
* React component for previewing and selecting new audio and video sources.
|
||||||
|
@ -96,417 +88,58 @@ class DeviceSelectionDialog extends Component {
|
||||||
* rendered. This is specifically used for hiding audio output on
|
* rendered. This is specifically used for hiding audio output on
|
||||||
* temasys browsers which do not support such change.
|
* temasys browsers which do not support such change.
|
||||||
*/
|
*/
|
||||||
hideAudioOutputSelect: React.PropTypes.bool,
|
hideAudioOutputSelect: React.PropTypes.bool
|
||||||
|
|
||||||
/**
|
|
||||||
* Invoked to obtain translated strings.
|
|
||||||
*/
|
|
||||||
t: React.PropTypes.func
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes a new DeviceSelectionDialog 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;
|
|
||||||
|
|
||||||
// Bind event handlers so they are only bound once for every instance.
|
|
||||||
this._closeModal = this._closeModal.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()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
return (
|
const {
|
||||||
<Dialog
|
currentAudioInputId,
|
||||||
cancelTitleKey = { 'dialog.Cancel' }
|
currentAudioOutputId,
|
||||||
okTitleKey = { 'dialog.Save' }
|
currentVideoInputId,
|
||||||
onCancel = { this._onCancel }
|
disableAudioInputChange,
|
||||||
onSubmit = { this._onSubmit }
|
disableDeviceChange,
|
||||||
titleKey = 'deviceSelection.deviceSettings' >
|
dispatch,
|
||||||
<div className = 'device-selection'>
|
hasAudioPermission,
|
||||||
<div className = 'device-selection-column column-video'>
|
hasVideoPermission,
|
||||||
<div className = 'device-selection-video-container'>
|
hideAudioInputPreview,
|
||||||
<VideoInputPreview
|
hideAudioOutputSelect
|
||||||
error = { this.state.previewVideoTrackError }
|
} = this.props;
|
||||||
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>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const props = {
|
||||||
* Cleans up preview tracks if they are not active tracks.
|
availableDevices: this.props._availableDevices,
|
||||||
*
|
closeModal: () => dispatch(hideDialog()),
|
||||||
* @private
|
currentAudioInputId,
|
||||||
* @returns {Array<Promise>} Zero to two promises will be returned. One
|
currentAudioOutputId,
|
||||||
* promise can be for video cleanup and another for audio cleanup.
|
currentVideoInputId,
|
||||||
*/
|
disableAudioInputChange,
|
||||||
_attemptPreviewTrackCleanup() {
|
disableDeviceChange,
|
||||||
return Promise.all([
|
hasAudioPermission,
|
||||||
this._disposeVideoPreview(),
|
hasVideoPermission,
|
||||||
this._disposeAudioPreview()
|
hideAudioInputPreview,
|
||||||
]);
|
hideAudioOutputSelect,
|
||||||
}
|
setAudioInputDevice: id => {
|
||||||
|
dispatch(setAudioInputDevice(id));
|
||||||
|
|
||||||
/**
|
return Promise.resolve();
|
||||||
* Signals to close DeviceSelectionDialog.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
_closeModal() {
|
|
||||||
this.props.dispatch(hideDialog());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disposes preview tracks and signals to close DeviceSelectionDialog.
|
|
||||||
*
|
|
||||||
* @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._closeModal)
|
|
||||||
.catch(this._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;
|
|
||||||
|
|
||||||
const deviceChangePromises = this._attemptPreviewTrackCleanup()
|
|
||||||
.then(() => {
|
|
||||||
if (this.state.selectedVideoInputId
|
|
||||||
!== this.props.currentVideoInputId) {
|
|
||||||
this.props.dispatch(
|
|
||||||
setVideoInputDevice(this.state.selectedVideoInputId));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.selectedAudioInputId
|
|
||||||
!== this.props.currentAudioInputId) {
|
|
||||||
this.props.dispatch(
|
|
||||||
setAudioInputDevice(this.state.selectedAudioInputId));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.selectedAudioOutputId
|
|
||||||
!== this.props.currentAudioOutputId) {
|
|
||||||
this.props.dispatch(
|
|
||||||
setAudioOutputDevice(this.state.selectedAudioOutputId));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all(deviceChangePromises)
|
|
||||||
.then(this._closeModal)
|
|
||||||
.catch(this._closeModal);
|
|
||||||
|
|
||||||
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} props - The props for the DeviceSelector.
|
|
||||||
* @returns {ReactElement}
|
|
||||||
*/
|
|
||||||
_renderSelector(props) {
|
|
||||||
return (
|
|
||||||
<DeviceSelector { ...props } />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
},
|
},
|
||||||
{
|
setAudioOutputDevice: id => {
|
||||||
devices: _availableDevices.audioInput,
|
dispatch(setAudioOutputDevice(id));
|
||||||
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) {
|
return Promise.resolve();
|
||||||
configurations.push({
|
},
|
||||||
devices: _availableDevices.audioOutput,
|
setVideoInputDevice: id => {
|
||||||
hasPermission: this.props.hasAudioPermission
|
dispatch(setVideoInputDevice(id));
|
||||||
|| 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(this._renderSelector);
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
return <DeviceSelectionDialogBase { ...props } />;
|
||||||
* 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 => {
|
|
||||||
this.setState({
|
|
||||||
previewVideoTrack: jitsiLocalTrack,
|
|
||||||
previewVideoTrackError: null
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
this.setState({
|
|
||||||
previewVideoTrack: null,
|
|
||||||
previewVideoTrackError:
|
|
||||||
this.props.t('deviceSelection.previewUnavailable')
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -526,4 +159,4 @@ function _mapStateToProps(state) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate(connect(_mapStateToProps)(DeviceSelectionDialog));
|
export default connect(_mapStateToProps)(DeviceSelectionDialog);
|
||||||
|
|
|
@ -0,0 +1,529 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { StatelessDialog } 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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: React.PropTypes.object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the dialog.
|
||||||
|
*/
|
||||||
|
closeModal: React.PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device id for the current audio input device. This device will be set
|
||||||
|
* as the default audio input device to preview.
|
||||||
|
*/
|
||||||
|
currentAudioInputId: React.PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device id for the current audio output device. This device will be
|
||||||
|
* set as the default audio output device to preview.
|
||||||
|
*/
|
||||||
|
currentAudioOutputId: React.PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device id for the current video input device. This device will be set
|
||||||
|
* as the default video input device to preview.
|
||||||
|
*/
|
||||||
|
currentVideoInputId: React.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: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables dismissing the dialog when the blanket is clicked. Enabled
|
||||||
|
* by default.
|
||||||
|
*/
|
||||||
|
disableBlanketClickDismiss: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if device changing is configured to be disallowed. Selectors
|
||||||
|
* will display as disabled.
|
||||||
|
*/
|
||||||
|
disableDeviceChange: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not a new audio input source can be selected.
|
||||||
|
*/
|
||||||
|
hasAudioPermission: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not a new video input sources can be selected.
|
||||||
|
*/
|
||||||
|
hasVideoPermission: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: React.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: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function that sets the audio input device.
|
||||||
|
*/
|
||||||
|
setAudioInputDevice: React.PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function that sets the audio output device.
|
||||||
|
*/
|
||||||
|
setAudioOutputDevice: React.PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function that sets the video input device.
|
||||||
|
*/
|
||||||
|
setVideoInputDevice: React.PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to obtain translated strings.
|
||||||
|
*/
|
||||||
|
t: React.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 = 'deviceSelection.deviceSettings'>
|
||||||
|
<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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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} props - The props for the DeviceSelector.
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
_renderSelector(props) {
|
||||||
|
return (
|
||||||
|
<DeviceSelector { ...props } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(this._renderSelector);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 => {
|
||||||
|
this.setState({
|
||||||
|
previewVideoTrack: jitsiLocalTrack,
|
||||||
|
previewVideoTrackError: null
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.setState({
|
||||||
|
previewVideoTrack: null,
|
||||||
|
previewVideoTrackError:
|
||||||
|
this.props.t('deviceSelection.previewUnavailable')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(DeviceSelectionDialogBase);
|
|
@ -83,7 +83,7 @@ class DeviceSelector extends Component {
|
||||||
return this._renderNoPermission();
|
return this._renderNoPermission();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.props.devices.length) {
|
if (!this.props.devices || !this.props.devices.length) {
|
||||||
return this._renderNoDevices();
|
return this._renderNoDevices();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
export { default as DeviceSelectionDialog } from './DeviceSelectionDialog';
|
export { default as DeviceSelectionDialog } from './DeviceSelectionDialog';
|
||||||
|
export { default as DeviceSelectionDialogBase }
|
||||||
|
from './DeviceSelectionDialogBase';
|
||||||
|
|
|
@ -1,2 +1,6 @@
|
||||||
export * from './actions';
|
export * from './actions';
|
||||||
|
export * from './actionTypes';
|
||||||
export * from './components';
|
export * from './components';
|
||||||
|
|
||||||
|
import './middleware';
|
||||||
|
import './reducer';
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { UPDATE_DEVICE_LIST } from '../base/devices';
|
||||||
|
import { MiddlewareRegistry } from '../base/redux';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the middleware of the feature device-selection.
|
||||||
|
*
|
||||||
|
* @param {Store} store - Redux store.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
MiddlewareRegistry.register(store => next => action => {
|
||||||
|
const result = next(action);
|
||||||
|
|
||||||
|
if (action.type === UPDATE_DEVICE_LIST) {
|
||||||
|
const { popupDialogData }
|
||||||
|
= store.getState()['features/device-selection'];
|
||||||
|
|
||||||
|
if (popupDialogData) {
|
||||||
|
popupDialogData.transport.sendEvent({ name: 'deviceListChanged' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
|
@ -0,0 +1,13 @@
|
||||||
|
import 'aui-css';
|
||||||
|
import 'aui-experimental-css';
|
||||||
|
|
||||||
|
import DeviceSelectionPopup from './DeviceSelectionPopup';
|
||||||
|
|
||||||
|
let deviceSelectionPopup;
|
||||||
|
|
||||||
|
window.init = function(i18next) {
|
||||||
|
deviceSelectionPopup = new DeviceSelectionPopup(i18next);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', () =>
|
||||||
|
deviceSelectionPopup.close());
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { ReducerRegistry } from '../base/redux';
|
||||||
|
|
||||||
|
import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen for actions which changes the state of the popup window for the device
|
||||||
|
* selection.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state of the feature
|
||||||
|
* features/device-selection.
|
||||||
|
* @param {Object} action - Action object.
|
||||||
|
* @param {string} action.type - Type of action.
|
||||||
|
* @param {Object} action.popupDialogData - Object that stores the current
|
||||||
|
* Window object of the popup and the Transport instance. If no popup is shown
|
||||||
|
* the value will be undefined.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
ReducerRegistry.register('features/device-selection',
|
||||||
|
(state = {}, action) => {
|
||||||
|
if (action.type === SET_DEVICE_SELECTION_POPUP_DATA) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
popupDialogData: action.popupDialogData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
});
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TouchableHighlight } from 'react-native';
|
import { TouchableHighlight } from 'react-native';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { Icon } from '../../base/font-icons';
|
import { Icon } from '../../base/font-icons';
|
||||||
|
|
||||||
|
@ -10,13 +11,20 @@ import AbstractToolbarButton from './AbstractToolbarButton';
|
||||||
*
|
*
|
||||||
* @extends AbstractToolbarButton
|
* @extends AbstractToolbarButton
|
||||||
*/
|
*/
|
||||||
export default class ToolbarButton extends AbstractToolbarButton {
|
class ToolbarButton extends AbstractToolbarButton {
|
||||||
/**
|
/**
|
||||||
* ToolbarButton component's property types.
|
* ToolbarButton component's property types.
|
||||||
*
|
*
|
||||||
* @static
|
* @static
|
||||||
*/
|
*/
|
||||||
static propTypes = AbstractToolbarButton.propTypes
|
static propTypes = {
|
||||||
|
...AbstractToolbarButton.propTypes,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to dispatch an action when the button is clicked.
|
||||||
|
*/
|
||||||
|
dispatch: React.PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the button of this Toolbar button.
|
* Renders the button of this Toolbar button.
|
||||||
|
@ -29,7 +37,13 @@ export default class ToolbarButton extends AbstractToolbarButton {
|
||||||
_renderButton(children) {
|
_renderButton(children) {
|
||||||
const props = {};
|
const props = {};
|
||||||
|
|
||||||
'onClick' in this.props && (props.onPress = this.props.onClick);
|
'onClick' in this.props && (props.onPress = () => {
|
||||||
|
const action = this.props.onClick(event);
|
||||||
|
|
||||||
|
if (action) {
|
||||||
|
this.props.dispatch(action);
|
||||||
|
}
|
||||||
|
});
|
||||||
'style' in this.props && (props.style = this.props.style);
|
'style' in this.props && (props.style = this.props.style);
|
||||||
'underlayColor' in this.props
|
'underlayColor' in this.props
|
||||||
&& (props.underlayColor = this.props.underlayColor);
|
&& (props.underlayColor = this.props.underlayColor);
|
||||||
|
@ -45,3 +59,5 @@ export default class ToolbarButton extends AbstractToolbarButton {
|
||||||
return super._renderIcon(Icon);
|
return super._renderIcon(Icon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect()(ToolbarButton);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { translate } from '../../base/i18n';
|
import { translate } from '../../base/i18n';
|
||||||
|
|
||||||
|
@ -36,6 +37,11 @@ class ToolbarButton extends AbstractToolbarButton {
|
||||||
*/
|
*/
|
||||||
button: React.PropTypes.object.isRequired,
|
button: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to dispatch an action when the button is clicked.
|
||||||
|
*/
|
||||||
|
dispatch: React.PropTypes.func,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for component mount.
|
* Handler for component mount.
|
||||||
*/
|
*/
|
||||||
|
@ -151,7 +157,11 @@ class ToolbarButton extends AbstractToolbarButton {
|
||||||
} = button;
|
} = button;
|
||||||
|
|
||||||
if (enabled && !unclickable && onClick) {
|
if (enabled && !unclickable && onClick) {
|
||||||
onClick(event);
|
const action = onClick(event);
|
||||||
|
|
||||||
|
if (action) {
|
||||||
|
this.props.dispatch(action);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,4 +238,4 @@ class ToolbarButton extends AbstractToolbarButton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate(ToolbarButton);
|
export default translate(connect()(ToolbarButton));
|
||||||
|
|
|
@ -2,25 +2,26 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { openDeviceSelectionDialog } from '../device-selection';
|
||||||
|
import { openDialOutDialog } from '../dial-out';
|
||||||
|
import { openInviteDialog } from '../invite';
|
||||||
import UIEvents from '../../../service/UI/UIEvents';
|
import UIEvents from '../../../service/UI/UIEvents';
|
||||||
|
|
||||||
import { openInviteDialog } from '../invite';
|
|
||||||
import { openDialOutDialog } from '../dial-out';
|
|
||||||
|
|
||||||
declare var APP: Object;
|
declare var APP: Object;
|
||||||
|
declare var interfaceConfig: Object;
|
||||||
declare var JitsiMeetJS: Object;
|
declare var JitsiMeetJS: Object;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All toolbar buttons' descriptors.
|
* All toolbar buttons' descriptors.
|
||||||
*/
|
*/
|
||||||
export default {
|
const buttons: Object = {
|
||||||
/**
|
/**
|
||||||
* The descriptor of the camera toolbar button.
|
* The descriptor of the camera toolbar button.
|
||||||
*/
|
*/
|
||||||
camera: {
|
camera: {
|
||||||
classNames: [ 'button', 'icon-camera' ],
|
classNames: [ 'button', 'icon-camera' ],
|
||||||
enabled: true,
|
enabled: true,
|
||||||
filmstripOnlyEnabled: true,
|
isDisplayed: () => true,
|
||||||
id: 'toolbar_button_camera',
|
id: 'toolbar_button_camera',
|
||||||
onClick() {
|
onClick() {
|
||||||
if (APP.conference.videoMuted) {
|
if (APP.conference.videoMuted) {
|
||||||
|
@ -153,11 +154,32 @@ export default {
|
||||||
id: 'toolbar_button_dial_out',
|
id: 'toolbar_button_dial_out',
|
||||||
onClick() {
|
onClick() {
|
||||||
JitsiMeetJS.analytics.sendEvent('toolbar.sip.clicked');
|
JitsiMeetJS.analytics.sendEvent('toolbar.sip.clicked');
|
||||||
APP.store.dispatch(openDialOutDialog());
|
|
||||||
|
return openDialOutDialog();
|
||||||
},
|
},
|
||||||
tooltipKey: 'dialOut.dialOut'
|
tooltipKey: 'dialOut.dialOut'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The descriptor of the device selection toolbar button.
|
||||||
|
*/
|
||||||
|
fodeviceselection: {
|
||||||
|
classNames: [ 'button', 'icon-settings' ],
|
||||||
|
enabled: true,
|
||||||
|
isDisplayed() {
|
||||||
|
return interfaceConfig.filmStripOnly;
|
||||||
|
},
|
||||||
|
id: 'toolbar_button_fodeviceselection',
|
||||||
|
onClick() {
|
||||||
|
JitsiMeetJS.analytics.sendEvent(
|
||||||
|
'toolbar.fodeviceselection.toggled');
|
||||||
|
|
||||||
|
return openDeviceSelectionDialog();
|
||||||
|
},
|
||||||
|
sideContainerId: 'settings_container',
|
||||||
|
tooltipKey: 'toolbar.Settings'
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The descriptor of the dialpad toolbar button.
|
* The descriptor of the dialpad toolbar button.
|
||||||
*/
|
*/
|
||||||
|
@ -217,7 +239,7 @@ export default {
|
||||||
hangup: {
|
hangup: {
|
||||||
classNames: [ 'button', 'icon-hangup', 'button_hangup' ],
|
classNames: [ 'button', 'icon-hangup', 'button_hangup' ],
|
||||||
enabled: true,
|
enabled: true,
|
||||||
filmstripOnlyEnabled: true,
|
isDisplayed: () => true,
|
||||||
id: 'toolbar_button_hangup',
|
id: 'toolbar_button_hangup',
|
||||||
onClick() {
|
onClick() {
|
||||||
JitsiMeetJS.analytics.sendEvent('toolbar.hangup');
|
JitsiMeetJS.analytics.sendEvent('toolbar.hangup');
|
||||||
|
@ -235,7 +257,8 @@ export default {
|
||||||
id: 'toolbar_button_link',
|
id: 'toolbar_button_link',
|
||||||
onClick() {
|
onClick() {
|
||||||
JitsiMeetJS.analytics.sendEvent('toolbar.invite.clicked');
|
JitsiMeetJS.analytics.sendEvent('toolbar.invite.clicked');
|
||||||
APP.store.dispatch(openInviteDialog());
|
|
||||||
|
return openInviteDialog();
|
||||||
},
|
},
|
||||||
tooltipKey: 'toolbar.invite'
|
tooltipKey: 'toolbar.invite'
|
||||||
},
|
},
|
||||||
|
@ -246,7 +269,7 @@ export default {
|
||||||
microphone: {
|
microphone: {
|
||||||
classNames: [ 'button', 'icon-microphone' ],
|
classNames: [ 'button', 'icon-microphone' ],
|
||||||
enabled: true,
|
enabled: true,
|
||||||
filmstripOnlyEnabled: true,
|
isDisplayed: () => true,
|
||||||
id: 'toolbar_button_mute',
|
id: 'toolbar_button_mute',
|
||||||
onClick() {
|
onClick() {
|
||||||
const sharedVideoManager = APP.UI.getSharedVideoManager();
|
const sharedVideoManager = APP.UI.getSharedVideoManager();
|
||||||
|
@ -386,3 +409,14 @@ export default {
|
||||||
tooltipKey: 'toolbar.sharedvideo'
|
tooltipKey: 'toolbar.sharedvideo'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Object.keys(buttons).forEach(name => {
|
||||||
|
const button = buttons[name];
|
||||||
|
|
||||||
|
if (!button.isDisplayed) {
|
||||||
|
button.isDisplayed = () => !interfaceConfig.filmStripOnly;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default buttons;
|
||||||
|
|
|
@ -64,7 +64,6 @@ export function getDefaultToolboxButtons(buttonHandlers: Object): Object {
|
||||||
|
|
||||||
if (typeof interfaceConfig !== 'undefined'
|
if (typeof interfaceConfig !== 'undefined'
|
||||||
&& interfaceConfig.TOOLBAR_BUTTONS) {
|
&& interfaceConfig.TOOLBAR_BUTTONS) {
|
||||||
const { filmStripOnly } = interfaceConfig;
|
|
||||||
|
|
||||||
toolbarButtons
|
toolbarButtons
|
||||||
= interfaceConfig.TOOLBAR_BUTTONS.reduce(
|
= interfaceConfig.TOOLBAR_BUTTONS.reduce(
|
||||||
|
@ -84,9 +83,9 @@ export function getDefaultToolboxButtons(buttonHandlers: Object): Object {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// In filmstrip-only mode we only add a button if it's
|
// If isDisplayed method is not defined, display the
|
||||||
// filmstrip-only enabled.
|
// button only for non-filmstripOnly mode
|
||||||
if (!filmStripOnly || button.filmstripOnlyEnabled) {
|
if (button.isDisplayed()) {
|
||||||
acc[place].set(buttonName, button);
|
acc[place].set(buttonName, button);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
SET_TOOLBOX_TIMEOUT_MS,
|
SET_TOOLBOX_TIMEOUT_MS,
|
||||||
SET_TOOLBOX_VISIBLE
|
SET_TOOLBOX_VISIBLE
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
|
import defaultToolbarButtons from './defaultToolbarButtons';
|
||||||
|
|
||||||
declare var interfaceConfig: Object;
|
declare var interfaceConfig: Object;
|
||||||
|
|
||||||
|
@ -208,6 +209,15 @@ ReducerRegistry.register(
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
function _setButton(state, { button, buttonName }): Object {
|
function _setButton(state, { button, buttonName }): Object {
|
||||||
|
const buttonDefinition = defaultToolbarButtons[buttonName];
|
||||||
|
|
||||||
|
// We don't need to update if the button shouldn't be displayed
|
||||||
|
if (!buttonDefinition || !buttonDefinition.isDisplayed()) {
|
||||||
|
return {
|
||||||
|
...state
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const { primaryToolbarButtons, secondaryToolbarButtons } = state;
|
const { primaryToolbarButtons, secondaryToolbarButtons } = state;
|
||||||
let selectedButton = primaryToolbarButtons.get(buttonName);
|
let selectedButton = primaryToolbarButtons.get(buttonName);
|
||||||
let place = 'primaryToolbarButtons';
|
let place = 'primaryToolbarButtons';
|
||||||
|
@ -222,18 +232,6 @@ function _setButton(state, { button, buttonName }): Object {
|
||||||
...button
|
...button
|
||||||
};
|
};
|
||||||
|
|
||||||
// In filmstrip-only mode we only show buttons if they're filmstrip-only
|
|
||||||
// enabled, so we don't need to update if this isn't the case.
|
|
||||||
// FIXME A reducer should be a pure function of the current state and the
|
|
||||||
// specified action so it should not use the global variable
|
|
||||||
// interfaceConfig. Anyway, we'll move interfaceConfig into the (redux)
|
|
||||||
// store so we'll surely revisit the source code bellow.
|
|
||||||
if (interfaceConfig.filmStripOnly && !selectedButton.filmstripOnlyEnabled) {
|
|
||||||
return {
|
|
||||||
...state
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedToolbar = state[place].set(buttonName, selectedButton);
|
const updatedToolbar = state[place].set(buttonName, selectedButton);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
<html itemscope itemtype="http://schema.org/Product" prefix="og: http://ogp.me/ns#" xmlns="http://www.w3.org/1999/html">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="content-type" content="text/html;charset=utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<base href="../" />
|
||||||
|
<!--#include virtual="/title.html" -->
|
||||||
|
<script><!--#include virtual="/interface_config.js" --></script>
|
||||||
|
<script>
|
||||||
|
window.config = {};
|
||||||
|
window.JitsiMeetJS = window.opener.window.JitsiMeetJS;
|
||||||
|
</script>
|
||||||
|
<script src="libs/device_selection_popup_bundle.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="css/all.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="react"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -193,6 +193,16 @@ const configs = [
|
||||||
output: Object.assign({}, config.output, {
|
output: Object.assign({}, config.output, {
|
||||||
library: 'JitsiMeetExternalAPI'
|
library: 'JitsiMeetExternalAPI'
|
||||||
})
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
// The Webpack configuration to bundle popup_bundle.js (js file for the
|
||||||
|
// device selection popup dialog).
|
||||||
|
Object.assign({}, config, {
|
||||||
|
entry: {
|
||||||
|
'device_selection_popup_bundle':
|
||||||
|
'./react/features/device-selection/popup.js'
|
||||||
|
},
|
||||||
|
output: config.output
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue