From cac8888b3734f86833d462c75dd70fde8c7737bd Mon Sep 17 00:00:00 2001 From: virtuacoplenny Date: Mon, 6 Aug 2018 08:24:59 -0700 Subject: [PATCH] feat(welcome-page): be able to open settings dialog (#3327) * feat(welcome-page): be able to open settings dialog - Create a getter for getting a settings tab's props so the device selection tab can get updated available devices. - Be able to call a function from a tab after it has mounted. This is used for device selection to essentially call enumerateDevices on the welcome page so the device selectors are populated. - Remove event UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED. Instead directly call setAudioOutputDeviceId where possible. - Fix initialization of the audioOutputDeviceId in settings by defaulting the audio output device to the one set in settings. * squash: updateAvailableDevices -> getAvailableDevices, add comment for propsUpdateFunction --- conference.js | 61 ++++++++----------- react/features/base/devices/actionTypes.js | 11 ---- react/features/base/devices/actions.js | 42 +++++++------ react/features/base/devices/middleware.js | 4 -- react/features/base/devices/reducer.js | 2 - .../dialog/components/DialogWithTabs.web.js | 25 +++++++- react/features/base/settings/reducer.js | 2 +- react/features/device-selection/actions.js | 38 +++++++++--- .../components/DeviceSelection.js | 20 ++++-- .../settings/components/web/SettingsDialog.js | 19 ++++++ react/features/settings/functions.js | 30 ++++++--- .../welcome/components/WelcomePage.web.js | 25 ++++++++ service/UI/UIEvents.js | 1 - 13 files changed, 186 insertions(+), 94 deletions(-) diff --git a/conference.js b/conference.js index f9b4a21c5..2241b802c 100644 --- a/conference.js +++ b/conference.js @@ -45,6 +45,7 @@ import { setDesktopSharingEnabled } from './react/features/base/conference'; import { + getAvailableDevices, setAudioOutputDeviceId, updateDeviceList } from './react/features/base/devices'; @@ -2129,20 +2130,6 @@ export default { } ); - APP.UI.addListener( - UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED, - audioOutputDeviceId => { - sendAnalytics(createDeviceChangedEvent('audio', 'output')); - setAudioOutputDeviceId(audioOutputDeviceId, APP.store.dispatch) - .then(() => logger.log('changed audio output device')) - .catch(err => { - logger.warn('Failed to change audio output device. ' - + 'Default or previously set audio output device ' - + 'will be used instead.', err); - }); - } - ); - APP.UI.addListener(UIEvents.TOGGLE_AUDIO_ONLY, audioOnly => { // FIXME On web video track is stored both in redux and in @@ -2314,39 +2301,43 @@ export default { /** * Inits list of current devices and event listener for device change. * @private + * @returns {Promise} */ _initDeviceList() { const { mediaDevices } = JitsiMeetJS; if (mediaDevices.isDeviceListAvailable() && mediaDevices.isDeviceChangeAvailable()) { - mediaDevices.enumerateDevices(devices => { - // Ugly way to synchronize real device IDs with local storage - // and settings menu. This is a workaround until - // getConstraints() method will be implemented in browsers. - const { dispatch } = APP.store; - - if (this.localAudio) { - dispatch(updateSettings({ - micDeviceId: this.localAudio.getDeviceId() - })); - } - if (this.localVideo) { - dispatch(updateSettings({ - cameraDeviceId: this.localVideo.getDeviceId() - })); - } - - APP.store.dispatch(updateDeviceList(devices)); - APP.UI.onAvailableDevicesChanged(devices); - }); - this.deviceChangeListener = devices => window.setTimeout(() => this._onDeviceListChanged(devices), 0); mediaDevices.addEventListener( JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED, this.deviceChangeListener); + + const { dispatch } = APP.store; + + return dispatch(getAvailableDevices()) + .then(devices => { + // Ugly way to synchronize real device IDs with local + // storage and settings menu. This is a workaround until + // getConstraints() method will be implemented in browsers. + if (this.localAudio) { + dispatch(updateSettings({ + micDeviceId: this.localAudio.getDeviceId() + })); + } + + if (this.localVideo) { + dispatch(updateSettings({ + cameraDeviceId: this.localVideo.getDeviceId() + })); + } + + APP.UI.onAvailableDevicesChanged(devices); + }); } + + return Promise.resolve(); }, /** diff --git a/react/features/base/devices/actionTypes.js b/react/features/base/devices/actionTypes.js index b2d17089b..62845dcfe 100644 --- a/react/features/base/devices/actionTypes.js +++ b/react/features/base/devices/actionTypes.js @@ -9,17 +9,6 @@ */ export const SET_AUDIO_INPUT_DEVICE = Symbol('SET_AUDIO_INPUT_DEVICE'); -/** - * The type of Redux action which signals that the currently used audio - * output device should be changed. - * - * { - * type: SET_AUDIO_OUTPUT_DEVICE, - * deviceId: string, - * } - */ -export const SET_AUDIO_OUTPUT_DEVICE = Symbol('SET_AUDIO_OUTPUT_DEVICE'); - /** * The type of Redux action which signals that the currently used video * input device should be changed. diff --git a/react/features/base/devices/actions.js b/react/features/base/devices/actions.js index 44d57d7ca..fd4fbfa2d 100644 --- a/react/features/base/devices/actions.js +++ b/react/features/base/devices/actions.js @@ -1,10 +1,34 @@ +import JitsiMeetJS from '../lib-jitsi-meet'; + import { SET_AUDIO_INPUT_DEVICE, - SET_AUDIO_OUTPUT_DEVICE, SET_VIDEO_INPUT_DEVICE, UPDATE_DEVICE_LIST } from './actionTypes'; +/** + * Queries for connected A/V input and output devices and updates the redux + * state of known devices. + * + * @returns {Function} + */ +export function getAvailableDevices() { + return dispatch => new Promise(resolve => { + const { mediaDevices } = JitsiMeetJS; + + if (mediaDevices.isDeviceListAvailable() + && mediaDevices.isDeviceChangeAvailable()) { + mediaDevices.enumerateDevices(devices => { + dispatch(updateDeviceList(devices)); + + resolve(devices); + }); + } else { + resolve([]); + } + }); +} + /** * Signals to update the currently used audio input device. * @@ -21,22 +45,6 @@ export function setAudioInputDevice(deviceId) { }; } -/** - * Signals to update the currently used audio output device. - * - * @param {string} deviceId - The id of the new audio ouput device. - * @returns {{ - * type: SET_AUDIO_OUTPUT_DEVICE, - * deviceId: string - * }} - */ -export function setAudioOutputDevice(deviceId) { - return { - type: SET_AUDIO_OUTPUT_DEVICE, - deviceId - }; -} - /** * Signals to update the currently used video input device. * diff --git a/react/features/base/devices/middleware.js b/react/features/base/devices/middleware.js index 69ce37b46..3fedd0f4f 100644 --- a/react/features/base/devices/middleware.js +++ b/react/features/base/devices/middleware.js @@ -6,7 +6,6 @@ import { MiddlewareRegistry } from '../redux'; import { SET_AUDIO_INPUT_DEVICE, - SET_AUDIO_OUTPUT_DEVICE, SET_VIDEO_INPUT_DEVICE } from './actionTypes'; @@ -22,9 +21,6 @@ MiddlewareRegistry.register(store => next => action => { case SET_AUDIO_INPUT_DEVICE: APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId); break; - case SET_AUDIO_OUTPUT_DEVICE: - APP.UI.emitEvent(UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED, action.deviceId); - break; case SET_VIDEO_INPUT_DEVICE: APP.UI.emitEvent(UIEvents.VIDEO_DEVICE_CHANGED, action.deviceId); break; diff --git a/react/features/base/devices/reducer.js b/react/features/base/devices/reducer.js index 0434c7ae9..2c7e396d1 100644 --- a/react/features/base/devices/reducer.js +++ b/react/features/base/devices/reducer.js @@ -1,6 +1,5 @@ import { SET_AUDIO_INPUT_DEVICE, - SET_AUDIO_OUTPUT_DEVICE, SET_VIDEO_INPUT_DEVICE, UPDATE_DEVICE_LIST } from './actionTypes'; @@ -40,7 +39,6 @@ ReducerRegistry.register( // now. case SET_AUDIO_INPUT_DEVICE: case SET_VIDEO_INPUT_DEVICE: - case SET_AUDIO_OUTPUT_DEVICE: default: return state; } diff --git a/react/features/base/dialog/components/DialogWithTabs.web.js b/react/features/base/dialog/components/DialogWithTabs.web.js index 5d25c9e09..217926ecf 100644 --- a/react/features/base/dialog/components/DialogWithTabs.web.js +++ b/react/features/base/dialog/components/DialogWithTabs.web.js @@ -103,6 +103,28 @@ class DialogWithTabs extends Component { ); } + /** + * Gets the props to pass into the tab component. + * + * @param {number} tabId - The index of the tab configuration within + * {@link this.state.tabStates}. + * @returns {Object} + */ + _getTabProps(tabId) { + const { tabs } = this.props; + const { tabStates } = this.state; + const tabConfiguration = tabs[tabId]; + const currentTabState = tabStates[tabId]; + + if (tabConfiguration.propsUpdateFunction) { + return tabConfiguration.propsUpdateFunction( + currentTabState, + tabConfiguration.props); + } + + return { ...currentTabState }; + } + /** * Renders the tabs from the tab information passed on props. * @@ -155,10 +177,11 @@ class DialogWithTabs extends Component {
+ { ...this._getTabProps(tabId) } />
); } diff --git a/react/features/base/settings/reducer.js b/react/features/base/settings/reducer.js index d38bb77f0..648ff8741 100644 --- a/react/features/base/settings/reducer.js +++ b/react/features/base/settings/reducer.js @@ -138,7 +138,7 @@ function _initSettings(featureState) { if (settings.audioOutputDeviceId !== JitsiMeetJS.mediaDevices.getAudioOutputDevice()) { JitsiMeetJS.mediaDevices.setAudioOutputDevice( - audioOutputDeviceId + settings.audioOutputDeviceId ).catch(ex => { logger.warn('Failed to set audio output device from local ' + 'storage. Default audio output device will be used' diff --git a/react/features/device-selection/actions.js b/react/features/device-selection/actions.js index 07408dc4c..4631210a0 100644 --- a/react/features/device-selection/actions.js +++ b/react/features/device-selection/actions.js @@ -4,18 +4,22 @@ import { Transport } from '../../../modules/transport'; +import { createDeviceChangedEvent, sendAnalytics } from '../analytics'; import { getAudioOutputDeviceId, setAudioInputDevice, - setAudioOutputDevice, + setAudioOutputDeviceId, setVideoInputDevice } from '../base/devices'; import { i18next } from '../base/i18n'; import JitsiMeetJS from '../base/lib-jitsi-meet'; +import { updateSettings } from '../base/settings'; import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes'; import { getDeviceSelectionDialogProps } from './functions'; +const logger = require('jitsi-meet-logger').getLogger(__filename); + /** * Opens a popup window with the device selection dialog in it. * @@ -115,23 +119,22 @@ function _processRequest(dispatch, getState, request, responseCallback) { // esl responseCallback(getState()['features/base/devices']); break; case 'setDevice': { - let action; const { device } = request; switch (device.kind) { case 'audioinput': - action = setAudioInputDevice; + dispatch(setAudioInputDevice(device.id)); break; case 'audiooutput': - action = setAudioOutputDevice; + setAudioOutputDeviceId(device.id, dispatch); break; case 'videoinput': - action = setVideoInputDevice; + dispatch(setVideoInputDevice(device.id)); break; default: } - dispatch(action(device.id)); + responseCallback(true); break; } @@ -179,6 +182,10 @@ export function submitDeviceSelectionTab(newState) { if (newState.selectedVideoInputId && newState.selectedVideoInputId !== currentState.selectedVideoInputId) { + dispatch(updateSettings({ + cameraDeviceId: newState.selectedVideoInputId + })); + dispatch( setVideoInputDevice(newState.selectedVideoInputId)); } @@ -186,6 +193,10 @@ export function submitDeviceSelectionTab(newState) { if (newState.selectedAudioInputId && newState.selectedAudioInputId !== currentState.selectedAudioInputId) { + dispatch(updateSettings({ + micDeviceId: newState.selectedAudioInputId + })); + dispatch( setAudioInputDevice(newState.selectedAudioInputId)); } @@ -193,8 +204,19 @@ export function submitDeviceSelectionTab(newState) { if (newState.selectedAudioOutputId && newState.selectedAudioOutputId !== currentState.selectedAudioOutputId) { - dispatch( - setAudioOutputDevice(newState.selectedAudioOutputId)); + sendAnalytics(createDeviceChangedEvent('audio', 'output')); + + setAudioOutputDeviceId( + newState.selectedAudioOutputId, + dispatch) + .then(() => logger.log('changed audio output device')) + .catch(err => { + logger.warn( + 'Failed to change audio output device.', + 'Default or previously set audio output device will', + ' be used instead.', + err); + }); } }; } diff --git a/react/features/device-selection/components/DeviceSelection.js b/react/features/device-selection/components/DeviceSelection.js index 59b700ef0..1f9358203 100644 --- a/react/features/device-selection/components/DeviceSelection.js +++ b/react/features/device-selection/components/DeviceSelection.js @@ -12,6 +12,8 @@ import AudioOutputPreview from './AudioOutputPreview'; import DeviceSelector from './DeviceSelector'; import VideoInputPreview from './VideoInputPreview'; +const logger = require('jitsi-meet-logger').getLogger(__filename); + /** * The type of the React {@code Component} props of {@link DeviceSelection}. */ @@ -64,6 +66,12 @@ export type Props = { */ hideAudioOutputSelect: boolean, + /** + * An optional callback to invoke after the component has completed its + * mount logic. + */ + mountCallback?: Function, + /** * The id of the audio input device to preview. */ @@ -134,8 +142,12 @@ class DeviceSelection extends AbstractDialogTab { * @inheritdoc */ componentDidMount() { - this._createAudioInputTrack(this.props.selectedAudioInputId); - this._createVideoInputTrack(this.props.selectedVideoInputId); + Promise.all([ + this._createAudioInputTrack(this.props.selectedAudioInputId), + this._createVideoInputTrack(this.props.selectedVideoInputId) + ]) + .catch(err => logger.warn('Failed to initialize preview tracks', err)) + .then(() => this.props.mountCallback && this.props.mountCallback()); } /** @@ -212,7 +224,7 @@ class DeviceSelection extends AbstractDialogTab { * @returns {void} */ _createAudioInputTrack(deviceId) { - this._disposeAudioInputPreview() + return this._disposeAudioInputPreview() .then(() => createLocalTrack('audio', deviceId)) .then(jitsiLocalTrack => { this.setState({ @@ -234,7 +246,7 @@ class DeviceSelection extends AbstractDialogTab { * @returns {void} */ _createVideoInputTrack(deviceId) { - this._disposeVideoInputPreview() + return this._disposeVideoInputPreview() .then(() => createLocalTrack('video', deviceId)) .then(jitsiLocalTrack => { if (!jitsiLocalTrack) { diff --git a/react/features/settings/components/web/SettingsDialog.js b/react/features/settings/components/web/SettingsDialog.js index a87c9c335..9f362ffbd 100644 --- a/react/features/settings/components/web/SettingsDialog.js +++ b/react/features/settings/components/web/SettingsDialog.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; +import { getAvailableDevices } from '../../../base/devices'; import { DialogWithTabs, hideDialog } from '../../../base/dialog'; import { DeviceSelection, @@ -77,6 +78,9 @@ class SettingsDialog extends Component { const tabs = _tabs.map(tab => { return { ...tab, + onMount: tab.onMount + ? (...args) => dispatch(tab.onMount(...args)) + : undefined, submit: (...args) => dispatch(tab.submit(...args)) }; }); @@ -133,7 +137,22 @@ function _mapStateToProps(state) { name: SETTINGS_TABS.DEVICES, component: DeviceSelection, label: 'settings.devices', + onMount: getAvailableDevices, props: getDeviceSelectionDialogProps(state), + propsUpdateFunction: (tabState, newProps) => { + // Ensure the device selection tab gets updated when new devices + // are found by taking the new props and only preserving the + // current user selected devices. If this were not done, the + // tab would keep using a copy of the initial props it received, + // leaving the device list to become stale. + + return { + ...newProps, + selectedAudioInputId: tabState.selectedAudioInputId, + selectedAudioOutputId: tabState.selectedAudioOutputId, + selectedVideoInputId: tabState.selectedVideoInputId + }; + }, styles: 'settings-pane devices-pane', submit: submitDeviceSelectionTab }); diff --git a/react/features/settings/functions.js b/react/features/settings/functions.js index 2c76ce136..317d11496 100644 --- a/react/features/settings/functions.js +++ b/react/features/settings/functions.js @@ -74,24 +74,30 @@ export function shouldShowOnlyDeviceSelection() { export function getMoreTabProps(stateful: Object | Function) { const state = toState(stateful); const language = i18next.language || DEFAULT_LANGUAGE; - const conference = state['features/base/conference']; + const { + conference, + followMeEnabled, + startAudioMutedPolicy, + startVideoMutedPolicy + } = state['features/base/conference']; const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || []; const localParticipant = getLocalParticipant(state); // The settings sections to display. - const showModeratorSettings - = configuredTabs.includes('moderator') - && localParticipant.role === PARTICIPANT_ROLE.MODERATOR; + const showModeratorSettings = Boolean( + conference + && configuredTabs.includes('moderator') + && localParticipant.role === PARTICIPANT_ROLE.MODERATOR); return { currentLanguage: language, - followMeEnabled: Boolean(conference.followMeEnabled), + followMeEnabled: Boolean(conference && followMeEnabled), languages: LANGUAGES, showLanguageSettings: configuredTabs.includes('language'), showModeratorSettings, - startAudioMuted: Boolean(conference.startAudioMutedPolicy), - startVideoMuted: Boolean(conference.startVideoMutedPolicy) + startAudioMuted: Boolean(conference && startAudioMutedPolicy), + startVideoMuted: Boolean(conference && startVideoMutedPolicy) }; } @@ -106,12 +112,16 @@ export function getMoreTabProps(stateful: Object | Function) { */ export function getProfileTabProps(stateful: Object | Function) { const state = toState(stateful); - const conference = state['features/base/conference']; + const { + authEnabled, + authLogin, + conference + } = state['features/base/conference']; const localParticipant = getLocalParticipant(state); return { - authEnabled: conference.authEnabled, - authLogin: conference.authLogin, + authEnabled: Boolean(conference && authEnabled), + authLogin: Boolean(conference && authLogin), displayName: localParticipant.name, email: localParticipant.email }; diff --git a/react/features/welcome/components/WelcomePage.web.js b/react/features/welcome/components/WelcomePage.web.js index 075693136..65877ece4 100644 --- a/react/features/welcome/components/WelcomePage.web.js +++ b/react/features/welcome/components/WelcomePage.web.js @@ -6,9 +6,11 @@ import { AtlasKitThemeProvider } from '@atlaskit/theme'; import React from 'react'; import { connect } from 'react-redux'; +import { DialogContainer } from '../../base/dialog'; import { translate } from '../../base/i18n'; import { Watermarks } from '../../base/react'; import { RecentList } from '../../recent-list'; +import { openSettingsDialog } from '../../settings'; import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage'; @@ -18,6 +20,15 @@ import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage'; * @extends AbstractWelcomePage */ class WelcomePage extends AbstractWelcomePage { + /** + * Default values for {@code WelcomePage} component's properties. + * + * @static + */ + static defaultProps = { + _room: '' + }; + /** * Initializes a new WelcomePage instance. * @@ -55,6 +66,7 @@ class WelcomePage extends AbstractWelcomePage { // Bind event handlers so they are only bound once per instance. this._onFormSubmit = this._onFormSubmit.bind(this); + this._onOpenSettings = this._onOpenSettings.bind(this); this._onRoomChange = this._onRoomChange.bind(this); this._setAdditionalContentRef = this._setAdditionalContentRef.bind(this); @@ -155,6 +167,9 @@ class WelcomePage extends AbstractWelcomePage { ref = { this._setAdditionalContentRef } /> : null } + + + ); } @@ -172,6 +187,16 @@ class WelcomePage extends AbstractWelcomePage { this._onJoin(); } + /** + * Opens {@code SettingsDialog}. + * + * @private + * @returns {void} + */ + _onOpenSettings() { + this.props.dispatch(openSettingsDialog()); + } + /** * Overrides the super to account for the differences in the argument types * provided by HTML and React Native text inputs. diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index 2fcda6a94..b23ff21a7 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -63,7 +63,6 @@ export default { LOGOUT: 'UI.logout', VIDEO_DEVICE_CHANGED: 'UI.video_device_changed', AUDIO_DEVICE_CHANGED: 'UI.audio_device_changed', - AUDIO_OUTPUT_DEVICE_CHANGED: 'UI.audio_output_device_changed', /** * Notifies interested listeners that the follow-me feature is enabled or