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
This commit is contained in:
virtuacoplenny 2018-08-06 08:24:59 -07:00 committed by yanas
parent 81853d971a
commit cac8888b37
13 changed files with 186 additions and 94 deletions

View File

@ -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.
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.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);
}
return Promise.resolve();
},
/**

View File

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

View File

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

View File

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

View File

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

View File

@ -103,6 +103,28 @@ class DialogWithTabs extends Component<Props, State> {
);
}
/**
* 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<Props, State> {
<div className = { styles }>
<TabComponent
closeDialog = { closeDialog }
mountCallback = { this.props.tabs[tabId].onMount }
onTabStateChange
= { this._onTabStateChange }
tabId = { tabId }
{ ...this.state.tabStates[tabId] } />
{ ...this._getTabProps(tabId) } />
</div>);
}

View File

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

View File

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

View File

@ -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<Props, State> {
* @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<Props, State> {
* @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<Props, State> {
* @returns {void}
*/
_createVideoInputTrack(deviceId) {
this._disposeVideoInputPreview()
return this._disposeVideoInputPreview()
.then(() => createLocalTrack('video', deviceId))
.then(jitsiLocalTrack => {
if (!jitsiLocalTrack) {

View File

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

View File

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

View File

@ -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 }
</div>
<AtlasKitThemeProvider mode = 'dark'>
<DialogContainer />
</AtlasKitThemeProvider>
</AtlasKitThemeProvider>
);
}
@ -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.

View File

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