feat(device_selection): Implement popup

This commit is contained in:
hristoterezov 2017-06-03 22:12:04 -05:00 committed by yanas
parent 2c002c875d
commit 96e83989a5
29 changed files with 1551 additions and 592 deletions

View File

@ -31,6 +31,8 @@ deploy-appbundle:
$(BUILD_DIR)/do_external_connect.min.map \
$(BUILD_DIR)/external_api.min.js \
$(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 \
$(DEPLOY_DIR)

View File

@ -136,7 +136,7 @@
&__videos-filmstripOnly {
margin-top: auto;
margin-bottom: auto;
padding-right: $defaultToolbarSize;
padding-right: $defaultFilmStripOnlyToolbarSize;
}
.remote-videos-container {

View File

@ -200,7 +200,13 @@
height: auto;
position: absolute;
right: 0;
width: $defaultToolbarSize;
width: $defaultFilmStripOnlyToolbarSize;
.button {
height: 37px;
line-height: 37px !important;
width: 37px;
}
.button:first-child {
border-top-left-radius: 3px;

View File

@ -34,6 +34,7 @@ $tooltipBg: rgba(0,0,0, 0.7);
* Toolbar
*/
$defaultToolbarSize: 50px;
$defaultFilmStripOnlyToolbarSize: 37px;
$splitterToolbarButtonMargin: 18px;
$toolbarBackground: rgba(0, 0, 0, 0.5);
$toolbarBadgeBackground: #165ECC;

View File

@ -36,14 +36,14 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars
*/
TOOLBAR_BUTTONS: [
//main toolbar
'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup',
'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup', 'deviceselection', // jshint ignore:line
//extended toolbar
'profile', 'contacts', 'chat', 'recording', 'etherpad', 'sharedvideo', 'dialout', 'settings', 'raisehand', 'filmstrip'], // jshint ignore:line
/**
* Main 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'],
// 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

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react';
import { hideDialog } from '../actions';
import { dialogPropTypes } from '../constants';
/**
* Abstract dialog to display dialogs.
@ -13,57 +14,12 @@ export default class AbstractDialog extends Component {
* @static
*/
static propTypes = {
/**
* 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,
...dialogPropTypes,
/**
* Used to show/hide the dialog on cancel.
*/
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
dispatch: React.PropTypes.func
};
/**

View File

@ -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 { connect } from 'react-redux';
import { translate } from '../../i18n';
import AbstractDialog from './AbstractDialog';
import StatelessDialog from './StatelessDialog';
/**
* Web dialog that uses atlaskit modal-dialog to display dialogs.
@ -19,6 +15,8 @@ class Dialog extends AbstractDialog {
* @static
*/
static propTypes = {
...AbstractDialog.propTypes,
/**
* This is the body of the dialog, the component children.
*/
@ -30,6 +28,11 @@ class Dialog extends AbstractDialog {
*/
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),
@ -40,6 +43,19 @@ class Dialog extends AbstractDialog {
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()}.
*
@ -47,101 +63,15 @@ class Dialog extends AbstractDialog {
* @returns {ReactElement}
*/
render() {
return (
<ModalDialog
footer = { this._renderFooter() }
header = { this._renderHeader() }
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>);
}
const props = {
...this.props,
onSubmit: this._onSubmit,
onCancel: 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;
}
delete props.dispatch;
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>
);
return <StatelessDialog { ...props } />;
}
/**
@ -158,4 +88,4 @@ class Dialog extends AbstractDialog {
}
}
export default translate(connect()(Dialog));
export default connect()(Dialog);

View File

@ -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);

View File

@ -1,2 +1,3 @@
export { default as DialogContainer } from './DialogContainer';
export { default as Dialog } from './Dialog';
export { default as StatelessDialog } from './StatelessDialog';

View File

@ -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
};

View File

@ -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 })
);
}
}

View File

@ -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');

View File

@ -1,8 +1,20 @@
/* globals APP */
/* globals APP, interfaceConfig */
import { openDialog } from '../base/dialog';
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';
/**
@ -13,6 +25,21 @@ import { DeviceSelectionDialog } from './components';
*/
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 => {
dispatch(openDialog(DeviceSelectionDialog, {
@ -33,5 +60,152 @@ export function openDeviceSelectionDialog() {
.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
};
}

View File

@ -53,7 +53,7 @@ class AudioInputPreview extends PureComponent {
*/
componentWillReceiveProps(nextProps) {
this._listenForAudioUpdates(nextProps.track);
this._updateAudioLevel(0);
this._updateAudioLevel(undefined, 0);
}
/**

View File

@ -6,17 +6,9 @@ import {
setAudioOutputDevice,
setVideoInputDevice
} from '../../base/devices';
import {
Dialog,
hideDialog
} from '../../base/dialog';
import { translate } from '../../base/i18n';
import { createLocalTrack } from '../../base/lib-jitsi-meet';
import { hideDialog } from '../../base/dialog';
import AudioInputPreview from './AudioInputPreview';
import AudioOutputPreview from './AudioOutputPreview';
import DeviceSelector from './DeviceSelector';
import VideoInputPreview from './VideoInputPreview';
import DeviceSelectionDialogBase from './DeviceSelectionDialogBase';
/**
* 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
* temasys browsers which do not support such change.
*/
hideAudioOutputSelect: React.PropTypes.bool,
/**
* Invoked to obtain translated strings.
*/
t: React.PropTypes.func
hideAudioOutputSelect: React.PropTypes.bool
};
/**
* 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()}.
*
* @inheritdoc
*/
render() {
return (
<Dialog
cancelTitleKey = { 'dialog.Cancel' }
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>
</Dialog>
);
}
const {
currentAudioInputId,
currentAudioOutputId,
currentVideoInputId,
disableAudioInputChange,
disableDeviceChange,
dispatch,
hasAudioPermission,
hasVideoPermission,
hideAudioInputPreview,
hideAudioOutputSelect
} = this.props;
/**
* 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()
]);
}
const props = {
availableDevices: this.props._availableDevices,
closeModal: () => dispatch(hideDialog()),
currentAudioInputId,
currentAudioOutputId,
currentVideoInputId,
disableAudioInputChange,
disableDeviceChange,
hasAudioPermission,
hasVideoPermission,
hideAudioInputPreview,
hideAudioOutputSelect,
setAudioInputDevice: id => {
dispatch(setAudioInputDevice(id));
/**
* 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
return Promise.resolve();
},
{
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
setAudioOutputDevice: id => {
dispatch(setAudioOutputDevice(id));
return Promise.resolve();
},
setVideoInputDevice: id => {
dispatch(setVideoInputDevice(id));
return Promise.resolve();
}
];
};
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);
}
/**
* 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')
});
});
});
return <DeviceSelectionDialogBase { ...props } />;
}
}
@ -526,4 +159,4 @@ function _mapStateToProps(state) {
};
}
export default translate(connect(_mapStateToProps)(DeviceSelectionDialog));
export default connect(_mapStateToProps)(DeviceSelectionDialog);

View File

@ -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);

View File

@ -83,7 +83,7 @@ class DeviceSelector extends Component {
return this._renderNoPermission();
}
if (!this.props.devices.length) {
if (!this.props.devices || !this.props.devices.length) {
return this._renderNoDevices();
}

View File

@ -1 +1,3 @@
export { default as DeviceSelectionDialog } from './DeviceSelectionDialog';
export { default as DeviceSelectionDialogBase }
from './DeviceSelectionDialogBase';

View File

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

View File

@ -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;
});

View File

@ -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());

View File

@ -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;
});

View File

@ -1,5 +1,6 @@
import React from 'react';
import { TouchableHighlight } from 'react-native';
import { connect } from 'react-redux';
import { Icon } from '../../base/font-icons';
@ -10,13 +11,20 @@ import AbstractToolbarButton from './AbstractToolbarButton';
*
* @extends AbstractToolbarButton
*/
export default class ToolbarButton extends AbstractToolbarButton {
class ToolbarButton extends AbstractToolbarButton {
/**
* ToolbarButton component's property types.
*
* @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.
@ -29,7 +37,13 @@ export default class ToolbarButton extends AbstractToolbarButton {
_renderButton(children) {
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);
'underlayColor' in this.props
&& (props.underlayColor = this.props.underlayColor);
@ -45,3 +59,5 @@ export default class ToolbarButton extends AbstractToolbarButton {
return super._renderIcon(Icon);
}
}
export default connect()(ToolbarButton);

View File

@ -1,6 +1,7 @@
/* @flow */
import React from 'react';
import { connect } from 'react-redux';
import { translate } from '../../base/i18n';
@ -36,6 +37,11 @@ class ToolbarButton extends AbstractToolbarButton {
*/
button: React.PropTypes.object.isRequired,
/**
* Used to dispatch an action when the button is clicked.
*/
dispatch: React.PropTypes.func,
/**
* Handler for component mount.
*/
@ -151,7 +157,11 @@ class ToolbarButton extends AbstractToolbarButton {
} = button;
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));

View File

@ -2,25 +2,26 @@
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 { openInviteDialog } from '../invite';
import { openDialOutDialog } from '../dial-out';
declare var APP: Object;
declare var interfaceConfig: Object;
declare var JitsiMeetJS: Object;
/**
* All toolbar buttons' descriptors.
*/
export default {
const buttons: Object = {
/**
* The descriptor of the camera toolbar button.
*/
camera: {
classNames: [ 'button', 'icon-camera' ],
enabled: true,
filmstripOnlyEnabled: true,
isDisplayed: () => true,
id: 'toolbar_button_camera',
onClick() {
if (APP.conference.videoMuted) {
@ -153,11 +154,32 @@ export default {
id: 'toolbar_button_dial_out',
onClick() {
JitsiMeetJS.analytics.sendEvent('toolbar.sip.clicked');
APP.store.dispatch(openDialOutDialog());
return openDialOutDialog();
},
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.
*/
@ -217,7 +239,7 @@ export default {
hangup: {
classNames: [ 'button', 'icon-hangup', 'button_hangup' ],
enabled: true,
filmstripOnlyEnabled: true,
isDisplayed: () => true,
id: 'toolbar_button_hangup',
onClick() {
JitsiMeetJS.analytics.sendEvent('toolbar.hangup');
@ -235,7 +257,8 @@ export default {
id: 'toolbar_button_link',
onClick() {
JitsiMeetJS.analytics.sendEvent('toolbar.invite.clicked');
APP.store.dispatch(openInviteDialog());
return openInviteDialog();
},
tooltipKey: 'toolbar.invite'
},
@ -246,7 +269,7 @@ export default {
microphone: {
classNames: [ 'button', 'icon-microphone' ],
enabled: true,
filmstripOnlyEnabled: true,
isDisplayed: () => true,
id: 'toolbar_button_mute',
onClick() {
const sharedVideoManager = APP.UI.getSharedVideoManager();
@ -386,3 +409,14 @@ export default {
tooltipKey: 'toolbar.sharedvideo'
}
};
Object.keys(buttons).forEach(name => {
const button = buttons[name];
if (!button.isDisplayed) {
button.isDisplayed = () => !interfaceConfig.filmStripOnly;
}
});
export default buttons;

View File

@ -64,7 +64,6 @@ export function getDefaultToolboxButtons(buttonHandlers: Object): Object {
if (typeof interfaceConfig !== 'undefined'
&& interfaceConfig.TOOLBAR_BUTTONS) {
const { filmStripOnly } = interfaceConfig;
toolbarButtons
= 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
// filmstrip-only enabled.
if (!filmStripOnly || button.filmstripOnlyEnabled) {
// If isDisplayed method is not defined, display the
// button only for non-filmstripOnly mode
if (button.isDisplayed()) {
acc[place].set(buttonName, button);
}
}

View File

@ -15,6 +15,7 @@ import {
SET_TOOLBOX_TIMEOUT_MS,
SET_TOOLBOX_VISIBLE
} from './actionTypes';
import defaultToolbarButtons from './defaultToolbarButtons';
declare var interfaceConfig: Object;
@ -208,6 +209,15 @@ ReducerRegistry.register(
* @returns {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;
let selectedButton = primaryToolbarButtons.get(buttonName);
let place = 'primaryToolbarButtons';
@ -222,18 +232,6 @@ function _setButton(state, { button, buttonName }): Object {
...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);
return {

View File

@ -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>

View File

@ -193,6 +193,16 @@ const configs = [
output: Object.assign({}, config.output, {
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
})
];