Make web use the redux settings/profile

This commit is contained in:
zbettenbuk 2018-04-12 21:58:20 +02:00 committed by Saúl Ibarra Corretgé
parent ab7e572162
commit 959db3a665
29 changed files with 455 additions and 434 deletions

2
app.js
View File

@ -15,7 +15,6 @@ import conference from './conference';
import API from './modules/API'; import API from './modules/API';
import keyboardshortcut from './modules/keyboardshortcut/keyboardshortcut'; import keyboardshortcut from './modules/keyboardshortcut/keyboardshortcut';
import remoteControl from './modules/remotecontrol/RemoteControl'; import remoteControl from './modules/remotecontrol/RemoteControl';
import settings from './modules/settings/Settings';
import translation from './modules/translation/translation'; import translation from './modules/translation/translation';
import UI from './modules/UI/UI'; import UI from './modules/UI/UI';
@ -41,7 +40,6 @@ window.APP = {
keyboardshortcut, keyboardshortcut,
remoteControl, remoteControl,
settings,
translation, translation,
UI UI
}; };

View File

@ -46,7 +46,10 @@ import {
sendLocalParticipant, sendLocalParticipant,
setDesktopSharingEnabled setDesktopSharingEnabled
} from './react/features/base/conference'; } from './react/features/base/conference';
import { updateDeviceList } from './react/features/base/devices'; import {
setAudioOutputDeviceId,
updateDeviceList
} from './react/features/base/devices';
import { import {
isFatalJitsiConnectionError, isFatalJitsiConnectionError,
JitsiConferenceErrors, JitsiConferenceErrors,
@ -82,6 +85,7 @@ import {
participantRoleChanged, participantRoleChanged,
participantUpdated participantUpdated
} from './react/features/base/participants'; } from './react/features/base/participants';
import { updateSettings } from './react/features/base/settings';
import { import {
createLocalTracksF, createLocalTracksF,
isLocalTrackMuted, isLocalTrackMuted,
@ -1273,7 +1277,7 @@ export default {
: 'colibri'; : 'colibri';
} }
const nick = APP.settings.getDisplayName(); const nick = APP.store.getState()['features/base/settings'].displayName;
if (nick) { if (nick) {
options.displayName = nick; options.displayName = nick;
@ -2131,7 +2135,9 @@ export default {
}) })
.then(() => { .then(() => {
logger.log('switched local video device'); logger.log('switched local video device');
APP.settings.setCameraDeviceId(cameraDeviceId, true); APP.store.dispatch(updateSettings({
cameraDeviceId
}));
}) })
.catch(err => { .catch(err => {
APP.UI.showCameraErrorNotification(err); APP.UI.showCameraErrorNotification(err);
@ -2163,7 +2169,9 @@ export default {
.then(stream => { .then(stream => {
this.useAudioStream(stream); this.useAudioStream(stream);
logger.log('switched local audio device'); logger.log('switched local audio device');
APP.settings.setMicDeviceId(micDeviceId, true); APP.store.dispatch(updateSettings({
micDeviceId
}));
}) })
.catch(err => { .catch(err => {
APP.UI.showMicErrorNotification(err); APP.UI.showMicErrorNotification(err);
@ -2175,7 +2183,7 @@ export default {
UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED, UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED,
audioOutputDeviceId => { audioOutputDeviceId => {
sendAnalytics(createDeviceChangedEvent('audio', 'output')); sendAnalytics(createDeviceChangedEvent('audio', 'output'));
APP.settings.setAudioOutputDeviceId(audioOutputDeviceId) setAudioOutputDeviceId(audioOutputDeviceId)
.then(() => logger.log('changed audio output device')) .then(() => logger.log('changed audio output device'))
.catch(err => { .catch(err => {
logger.warn('Failed to change audio output device. ' logger.warn('Failed to change audio output device. '
@ -2317,7 +2325,8 @@ export default {
APP.store.dispatch(conferenceJoined(room)); APP.store.dispatch(conferenceJoined(room));
APP.UI.mucJoined(); APP.UI.mucJoined();
const displayName = APP.settings.getDisplayName(); const displayName
= APP.store.getState()['features/base/settings'].displayName;
APP.API.notifyConferenceJoined( APP.API.notifyConferenceJoined(
this.roomName, this.roomName,
@ -2367,14 +2376,18 @@ export default {
// storage and settings menu. This is a workaround until // storage and settings menu. This is a workaround until
// getConstraints() method will be implemented // getConstraints() method will be implemented
// in browsers. // in browsers.
const { dispatch } = APP.store;
if (this.localAudio) { if (this.localAudio) {
APP.settings.setMicDeviceId( dispatch(updateSettings({
this.localAudio.getDeviceId(), false); micDeviceId: this.localAudio.getDeviceId()
}));
} }
if (this.localVideo) { if (this.localVideo) {
APP.settings.setCameraDeviceId( dispatch(updateSettings({
this.localVideo.getDeviceId(), false); cameraDeviceId: this.localVideo.getDeviceId()
}));
} }
mediaDeviceHelper.setCurrentMediaDevices(devices); mediaDeviceHelper.setCurrentMediaDevices(devices);
@ -2426,8 +2439,7 @@ export default {
if (typeof newDevices.audiooutput !== 'undefined') { if (typeof newDevices.audiooutput !== 'undefined') {
// Just ignore any errors in catch block. // Just ignore any errors in catch block.
promises.push(APP.settings promises.push(setAudioOutputDeviceId(newDevices.audiooutput)
.setAudioOutputDeviceId(newDevices.audiooutput)
.catch()); .catch());
} }
@ -2576,7 +2588,10 @@ export default {
email: formattedEmail email: formattedEmail
})); }));
APP.settings.setEmail(formattedEmail); APP.store.dispatch(updateSettings({
email: formattedEmail
}));
APP.UI.setUserEmail(localId, formattedEmail); APP.UI.setUserEmail(localId, formattedEmail);
sendData(commands.EMAIL, formattedEmail); sendData(commands.EMAIL, formattedEmail);
}, },
@ -2600,7 +2615,9 @@ export default {
avatarURL: formattedUrl avatarURL: formattedUrl
})); }));
APP.settings.setAvatarUrl(url); APP.store.dispatch(updateSettings({
avatarURL: formattedUrl
}));
sendData(commands.AVATAR_URL, url); sendData(commands.AVATAR_URL, url);
}, },
@ -2654,7 +2671,10 @@ export default {
name: formattedNickname name: formattedNickname
})); }));
APP.settings.setDisplayName(formattedNickname); APP.store.dispatch(updateSettings({
displayName: formattedNickname
}));
APP.API.notifyDisplayNameChanged(id, { APP.API.notifyDisplayNameChanged(id, {
displayName: formattedNickname, displayName: formattedNickname,
formattedDisplayName: formattedDisplayName:

View File

@ -1,7 +1,6 @@
/* global $, APP */ /* global $, APP */
import UIUtil from '../../util/UIUtil'; import UIUtil from '../../util/UIUtil';
import UIEvents from '../../../../service/UI/UIEvents'; import UIEvents from '../../../../service/UI/UIEvents';
import Settings from '../../../settings/Settings';
import { import {
createProfilePanelButtonEvent, createProfilePanelButtonEvent,
@ -54,6 +53,8 @@ export default {
init(emitter) { init(emitter) {
initHTML(); initHTML();
const settings = APP.store.getState()['features/base/settings'];
/** /**
* Updates display name. * Updates display name.
* *
@ -64,7 +65,7 @@ export default {
} }
$('#setDisplayName') $('#setDisplayName')
.val(Settings.getDisplayName()) .val(settings.displayName)
.keyup(event => { .keyup(event => {
if (event.keyCode === 13) { // enter if (event.keyCode === 13) { // enter
updateDisplayName(); updateDisplayName();
@ -82,7 +83,7 @@ export default {
} }
$('#setEmail') $('#setEmail')
.val(Settings.getEmail()) .val(settings.email)
.keyup(event => { .keyup(event => {
if (event.keyCode === 13) { // enter if (event.keyCode === 13) { // enter
updateEmail(); updateEmail();

View File

@ -74,17 +74,6 @@ const UIUtil = {
.html(); .html();
}, },
/**
* Unescapes the given text.
*
* @param {string} safe string which contains escaped html
* @returns {string} unescaped html string.
*/
unescapeHtml(safe) {
return $('<div />').html(safe)
.text();
},
imageToGrayScale(canvas) { imageToGrayScale(canvas) {
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
const imgData = context.getImageData(0, 0, canvas.width, canvas.height); const imgData = context.getImageData(0, 0, canvas.width, canvas.height);

View File

@ -10,6 +10,7 @@ import { VideoTrack } from '../../../react/features/base/media';
import { import {
getAvatarURLByParticipantId getAvatarURLByParticipantId
} from '../../../react/features/base/participants'; } from '../../../react/features/base/participants';
import { updateSettings } from '../../../react/features/base/settings';
/* eslint-enable no-unused-vars */ /* eslint-enable no-unused-vars */
const logger = require('jitsi-meet-logger').getLogger(__filename); const logger = require('jitsi-meet-logger').getLogger(__filename);
@ -121,9 +122,10 @@ LocalVideo.prototype.changeVideo = function(stream) {
// eslint-disable-next-line eqeqeq // eslint-disable-next-line eqeqeq
const isVideo = stream.videoType != 'desktop'; const isVideo = stream.videoType != 'desktop';
const settings = APP.store.getState()['features/base/settings'];
this._enableDisableContextMenu(isVideo); this._enableDisableContextMenu(isVideo);
this.setFlipX(isVideo ? APP.settings.getLocalFlipX() : false); this.setFlipX(isVideo ? settings.localFlipX : false);
const endedHandler = () => { const endedHandler = () => {
@ -194,10 +196,14 @@ LocalVideo.prototype._buildContextMenu = function() {
flip: { flip: {
name: 'Flip', name: 'Flip',
callback: () => { callback: () => {
const val = !APP.settings.getLocalFlipX(); const { store } = APP;
const val = !store.getState()['features/base/settings']
.localFlipX;
this.setFlipX(val); this.setFlipX(val);
APP.settings.setLocalFlipX(val); store.dispatch(updateSettings({
localFlipX: val
}));
} }
} }
}, },

View File

@ -1,5 +1,7 @@
/* global APP, JitsiMeetJS */ /* global APP, JitsiMeetJS */
import { getAudioOutputDeviceId } from '../../react/features/base/devices';
let currentAudioInputDevices, let currentAudioInputDevices,
currentAudioOutputDevices, currentAudioOutputDevices,
currentVideoInputDevices; currentVideoInputDevices;
@ -16,7 +18,7 @@ function getNewAudioOutputDevice(newDevices) {
return; return;
} }
const selectedAudioOutputDeviceId = APP.settings.getAudioOutputDeviceId(); const selectedAudioOutputDeviceId = getAudioOutputDeviceId();
const availableAudioOutputDevices = newDevices.filter( const availableAudioOutputDevices = newDevices.filter(
d => d.kind === 'audiooutput'); d => d.kind === 'audiooutput');
@ -40,7 +42,8 @@ function getNewAudioOutputDevice(newDevices) {
function getNewAudioInputDevice(newDevices, localAudio) { function getNewAudioInputDevice(newDevices, localAudio) {
const availableAudioInputDevices = newDevices.filter( const availableAudioInputDevices = newDevices.filter(
d => d.kind === 'audioinput'); d => d.kind === 'audioinput');
const selectedAudioInputDeviceId = APP.settings.getMicDeviceId(); const settings = APP.store.getState()['features/base/settings'];
const selectedAudioInputDeviceId = settings.micDeviceId;
const selectedAudioInputDevice = availableAudioInputDevices.find( const selectedAudioInputDevice = availableAudioInputDevices.find(
d => d.deviceId === selectedAudioInputDeviceId); d => d.deviceId === selectedAudioInputDeviceId);
@ -76,7 +79,8 @@ function getNewAudioInputDevice(newDevices, localAudio) {
function getNewVideoInputDevice(newDevices, localVideo) { function getNewVideoInputDevice(newDevices, localVideo) {
const availableVideoInputDevices = newDevices.filter( const availableVideoInputDevices = newDevices.filter(
d => d.kind === 'videoinput'); d => d.kind === 'videoinput');
const selectedVideoInputDeviceId = APP.settings.getCameraDeviceId(); const settings = APP.store.getState()['features/base/settings'];
const selectedVideoInputDeviceId = settings.cameraDeviceId;
const selectedVideoInputDevice = availableVideoInputDevices.find( const selectedVideoInputDevice = availableVideoInputDevices.find(
d => d.deviceId === selectedVideoInputDeviceId); d => d.deviceId === selectedVideoInputDeviceId);

View File

@ -1,191 +0,0 @@
/* global JitsiMeetJS */
const logger = require('jitsi-meet-logger').getLogger(__filename);
import UIUtil from '../UI/util/UIUtil';
import jitsiLocalStorage from '../util/JitsiLocalStorage';
import { randomHexString } from '../../react/features/base/util';
let avatarUrl = '';
let email = UIUtil.unescapeHtml(jitsiLocalStorage.getItem('email') || '');
let avatarId = UIUtil.unescapeHtml(jitsiLocalStorage.getItem('avatarId') || '');
if (!avatarId) {
// if there is no avatar id, we generate a unique one and use it forever
avatarId = randomHexString(32);
jitsiLocalStorage.setItem('avatarId', avatarId);
}
let localFlipX = JSON.parse(jitsiLocalStorage.getItem('localFlipX') || true);
let displayName = UIUtil.unescapeHtml(
jitsiLocalStorage.getItem('displayname') || '');
let cameraDeviceId = jitsiLocalStorage.getItem('cameraDeviceId') || '';
let micDeviceId = jitsiLocalStorage.getItem('micDeviceId') || '';
// Currently audio output device change is supported only in Chrome and
// default output always has 'default' device ID
const audioOutputDeviceId = jitsiLocalStorage.getItem('audioOutputDeviceId')
|| 'default';
if (audioOutputDeviceId
!== JitsiMeetJS.mediaDevices.getAudioOutputDevice()) {
JitsiMeetJS.mediaDevices.setAudioOutputDevice(audioOutputDeviceId)
.catch(ex => {
logger.warn('Failed to set audio output device from local '
+ 'storage. Default audio output device will be used'
+ 'instead.', ex);
});
}
export default {
/**
* Sets the local user display name and saves it to local storage
*
* @param {string} newDisplayName unescaped display name for the local user
* @param {boolean} disableLocalStore disables local store the display name
*/
setDisplayName(newDisplayName, disableLocalStore) {
displayName = newDisplayName;
if (!disableLocalStore) {
jitsiLocalStorage.setItem('displayname',
UIUtil.escapeHtml(displayName));
}
},
/**
* Returns the escaped display name currently used by the user
* @returns {string} currently valid user display name.
*/
getDisplayName() {
return displayName;
},
/**
* Sets new email for local user and saves it to the local storage.
* @param {string} newEmail new email for the local user
* @param {boolean} disableLocalStore disables local store the email
*/
setEmail(newEmail, disableLocalStore) {
email = newEmail;
if (!disableLocalStore) {
jitsiLocalStorage.setItem('email', UIUtil.escapeHtml(newEmail));
}
},
/**
* Returns email address of the local user.
* @returns {string} email
*/
getEmail() {
return email;
},
/**
* Returns avatar id of the local user.
* @returns {string} avatar id
*/
getAvatarId() {
return avatarId;
},
/**
* Sets new avatarUrl for local user and saves it to the local storage.
* @param {string} newAvatarUrl new avatarUrl for the local user
*/
setAvatarUrl(newAvatarUrl) {
avatarUrl = newAvatarUrl;
},
/**
* Returns avatarUrl address of the local user.
* @returns {string} avatarUrl
*/
getAvatarUrl() {
return avatarUrl;
},
/**
* Sets new flipX state of local video and saves it to the local storage.
* @param {string} val flipX state of local video
*/
setLocalFlipX(val) {
localFlipX = val;
jitsiLocalStorage.setItem('localFlipX', val);
},
/**
* Returns flipX state of local video.
* @returns {string} flipX
*/
getLocalFlipX() {
return localFlipX;
},
/**
* Get device id of the camera which is currently in use.
* Empty string stands for default device.
* @returns {String}
*/
getCameraDeviceId() {
return cameraDeviceId;
},
/**
* Set device id of the camera which is currently in use.
* Empty string stands for default device.
* @param {string} newId new camera device id
* @param {boolean} whether we need to store the value
*/
setCameraDeviceId(newId, store) {
cameraDeviceId = newId;
if (store) {
jitsiLocalStorage.setItem('cameraDeviceId', newId);
}
},
/**
* Get device id of the microphone which is currently in use.
* Empty string stands for default device.
* @returns {String}
*/
getMicDeviceId() {
return micDeviceId;
},
/**
* Set device id of the microphone which is currently in use.
* Empty string stands for default device.
* @param {string} newId new microphone device id
* @param {boolean} whether we need to store the value
*/
setMicDeviceId(newId, store) {
micDeviceId = newId;
if (store) {
jitsiLocalStorage.setItem('micDeviceId', newId);
}
},
/**
* Get device id of the audio output device which is currently in use.
* Empty string stands for default device.
* @returns {String}
*/
getAudioOutputDeviceId() {
return JitsiMeetJS.mediaDevices.getAudioOutputDevice();
},
/**
* Set device id of the audio output device which is currently in use.
* Empty string stands for default device.
* @param {string} newId='default' - new audio output device id
* @returns {Promise}
*/
setAudioOutputDeviceId(newId = 'default') {
return JitsiMeetJS.mediaDevices.setAudioOutputDevice(newId)
.then(() =>
jitsiLocalStorage.setItem('audioOutputDeviceId', newId));
}
};

View File

@ -12,7 +12,6 @@ import {
localParticipantJoined, localParticipantJoined,
localParticipantLeft localParticipantLeft
} from '../../base/participants'; } from '../../base/participants';
import '../../base/profile';
import { Fragment, RouteRegistry } from '../../base/react'; import { Fragment, RouteRegistry } from '../../base/react';
import { MiddlewareRegistry, ReducerRegistry } from '../../base/redux'; import { MiddlewareRegistry, ReducerRegistry } from '../../base/redux';
import { SoundCollection } from '../../base/sounds'; import { SoundCollection } from '../../base/sounds';
@ -26,6 +25,9 @@ import { appNavigate, appWillMount, appWillUnmount } from '../actions';
/** /**
* The default URL to open if no other was specified to {@code AbstractApp} * The default URL to open if no other was specified to {@code AbstractApp}
* via props. * via props.
*
* FIXME: This is not at the best place here. This should be either in the
* base/settings feature or a default in base/config.
*/ */
const DEFAULT_URL = 'https://meet.jit.si'; const DEFAULT_URL = 'https://meet.jit.si';
@ -133,26 +135,14 @@ export class AbstractApp extends Component {
// actions is important, not the call site. Moreover, we've got // actions is important, not the call site. Moreover, we've got
// localParticipant business logic in the React Component // localParticipant business logic in the React Component
// (i.e. UI) AbstractApp now. // (i.e. UI) AbstractApp now.
let localParticipant = {};
if (typeof APP === 'object') { const settings = getState()['features/base/settings'];
localParticipant = { const localParticipant = {
avatarID: APP.settings.getAvatarId(), avatarID: settings.avatarID,
avatarURL: APP.settings.getAvatarUrl(), avatarURL: settings.avatarURL,
email: APP.settings.getEmail(), email: settings.email,
name: APP.settings.getDisplayName() name: settings.displayName
}; };
}
// Profile is the new React compatible settings.
const profile = getState()['features/base/profile'];
if (profile) {
localParticipant.email
= profile.email || localParticipant.email;
localParticipant.name
= profile.displayName || localParticipant.name;
}
// We set the initialized state here and not in the contructor to // We set the initialized state here and not in the contructor to
// make sure that {@code componentWillMount} gets invoked before // make sure that {@code componentWillMount} gets invoked before
@ -383,7 +373,7 @@ export class AbstractApp extends Component {
return ( return (
this.props.defaultURL this.props.defaultURL
|| this._getStore().getState()['features/base/profile'] || this._getStore().getState()['features/base/settings']
.serverURL .serverURL
|| DEFAULT_URL); || DEFAULT_URL);
} }

View File

@ -0,0 +1,32 @@
// @flow
import JitsiMeetJS from '../lib-jitsi-meet';
import { updateSettings } from '../settings';
/**
* Get device id of the audio output device which is currently in use.
* Empty string stands for default device.
*
* @returns {string}
*/
export function getAudioOutputDeviceId() {
return JitsiMeetJS.mediaDevices.getAudioOutputDevice();
}
/**
* Set device id of the audio output device which is currently in use.
* Empty string stands for default device.
*
* @param {string} newId - New audio output device id.
* @param {Function} dispatch - The Redux dispatch function.
* @returns {Promise}
*/
export function setAudioOutputDeviceId(
newId: string = 'default',
dispatch: Function): Promise<*> {
return JitsiMeetJS.mediaDevices.setAudioOutputDevice(newId)
.then(() =>
dispatch(updateSettings({
audioOutputDeviceId: newId
})));
}

View File

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

View File

@ -8,8 +8,8 @@ import {
} from '../../analytics'; } from '../../analytics';
import { isRoomValid, SET_ROOM, setAudioOnly } from '../conference'; import { isRoomValid, SET_ROOM, setAudioOnly } from '../conference';
import JitsiMeetJS from '../lib-jitsi-meet'; import JitsiMeetJS from '../lib-jitsi-meet';
import { getPropertyValue } from '../profile';
import { MiddlewareRegistry } from '../redux'; import { MiddlewareRegistry } from '../redux';
import { getPropertyValue } from '../settings';
import { setTrackMuted, TRACK_ADDED } from '../tracks'; import { setTrackMuted, TRACK_ADDED } from '../tracks';
import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions'; import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions';
@ -79,11 +79,11 @@ function _setRoom({ dispatch, getState }, next, action) {
const mutedSources = { const mutedSources = {
// We have startWithAudioMuted and startWithVideoMuted here: // We have startWithAudioMuted and startWithVideoMuted here:
config: true, config: true,
profile: true, settings: true,
// XXX We've already overwritten base/config with urlParams. However, // XXX We've already overwritten base/config with urlParams. However,
// profile is more important than the server-side config. Consequently, // settings are more important than the server-side config.
// we need to read from urlParams anyway: // Consequently, we need to read from urlParams anyway:
urlParams: true, urlParams: true,
// We don't have startWithAudioMuted and startWithVideoMuted here: // We don't have startWithAudioMuted and startWithVideoMuted here:
@ -141,7 +141,7 @@ function _setRoom({ dispatch, getState }, next, action) {
config: roomIsValid, config: roomIsValid,
// XXX We've already overwritten base/config with // XXX We've already overwritten base/config with
// urlParams if roomIsValid. However, profile is more // urlParams if roomIsValid. However, settings are more
// important than the server-side config. Consequently, // important than the server-side config. Consequently,
// we need to read from urlParams anyway. We also // we need to read from urlParams anyway. We also
// probably want to read from urlParams when // probably want to read from urlParams when
@ -151,7 +151,7 @@ function _setRoom({ dispatch, getState }, next, action) {
// The following don't have complications around whether // The following don't have complications around whether
// they are defined or not: // they are defined or not:
jwt: false, jwt: false,
profile: true settings: true
})); }));
} else { } else {
// Default to audio-only if the (execution) environment does not // Default to audio-only if the (execution) environment does not

View File

@ -1,15 +0,0 @@
/**
* Create an action for when the local profile is updated.
*
* {
* type: PROFILE_UPDATED,
* profile: {
* displayName: string,
* defaultURL: URL,
* email: string,
* startWithAudioMuted: boolean,
* startWithVideoMuted: boolean
* }
* }
*/
export const PROFILE_UPDATED = Symbol('PROFILE_UPDATED');

View File

@ -1,23 +0,0 @@
import { PROFILE_UPDATED } from './actionTypes';
/**
* Create an action for when the local profile is updated.
*
* @param {Object} profile - The new profile data.
* @returns {{
* type: UPDATE_PROFILE,
* profile: {
* displayName: string,
* defaultURL: URL,
* email: string,
* startWithAudioMuted: boolean,
* startWithVideoMuted: boolean
* }
* }}
*/
export function updateProfile(profile) {
return {
type: PROFILE_UPDATED,
profile
};
}

View File

@ -1,53 +0,0 @@
// @flow
import { APP_WILL_MOUNT } from '../../app';
import { ReducerRegistry } from '../redux';
import { PersistenceRegistry } from '../storage';
import { PROFILE_UPDATED } from './actionTypes';
/**
* The default/initial redux state of the feature {@code base/profile}.
*
* @type Object
*/
const DEFAULT_STATE = {};
const STORE_NAME = 'features/base/profile';
/**
* Sets up the persistence of the feature {@code base/profile}.
*/
PersistenceRegistry.register(STORE_NAME);
ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
switch (action.type) {
case APP_WILL_MOUNT:
// XXX APP_WILL_MOUNT is the earliest redux action of ours dispatched in
// the store. For the purposes of legacy support, make sure that the
// deserialized base/profile's state is in the format deemed current by
// the current app revision.
if (state && typeof state === 'object') {
// In an enterprise/internal build of Jitsi Meet for Android and iOS
// we had base/profile's state as an object with property profile.
const { profile } = state;
if (profile && typeof profile === 'object') {
return { ...profile };
}
} else {
// In the weird case that we have previously persisted/serialized
// null.
return DEFAULT_STATE;
}
break;
case PROFILE_UPDATED:
return {
...state,
...action.profile
};
}
return state;
});

View File

@ -0,0 +1,22 @@
/**
* Create an action for when the settings are updated.
*
* {
* type: SETTINGS_UPDATED,
* settings: {
* audioOutputDeviceId: string,
* avatarID: string,
* avatarURL: string,
* cameraDeviceId: string,
* displayName: string,
* email: string,
* localFlipX: boolean,
* micDeviceId: string,
* serverURL: string,
* startAudioOnly: boolean,
* startWithAudioMuted: boolean,
* startWithVideoMuted: boolean
* }
* }
*/
export const SETTINGS_UPDATED = Symbol('SETTINGS_UPDATED');

View File

@ -0,0 +1,30 @@
import { SETTINGS_UPDATED } from './actionTypes';
/**
* Create an action for when the settings are updated.
*
* @param {Object} settings - The new (partial) settings properties.
* @returns {{
* type: SETTINGS_UPDATED,
* settings: {
* audioOutputDeviceId: string,
* avatarID: string,
* avatarURL: string,
* cameraDeviceId: string,
* displayName: string,
* email: string,
* localFlipX: boolean,
* micDeviceId: string,
* serverURL: string,
* startAudioOnly: boolean,
* startWithAudioMuted: boolean,
* startWithVideoMuted: boolean
* }
* }}
*/
export function updateSettings(settings) {
return {
type: SETTINGS_UPDATED,
settings
};
}

View File

@ -3,9 +3,11 @@
import { parseURLParams } from '../config'; import { parseURLParams } from '../config';
import { toState } from '../redux'; import { toState } from '../redux';
/** /**
* Returns the effective value of a configuration/preference/setting by applying * Returns the effective value of a configuration/preference/setting by applying
* a precedence among the values specified by JWT, URL, profile, and config. * a precedence among the values specified by JWT, URL, settings,
* and config.
* *
* @param {Object|Function} stateful - The redux state object or * @param {Object|Function} stateful - The redux state object or
* {@code getState} function. * {@code getState} function.
@ -14,7 +16,7 @@ import { toState } from '../redux';
* @param {{ * @param {{
* config: boolean, * config: boolean,
* jwt: boolean, * jwt: boolean,
* profile: boolean, * settings: boolean,
* urlParams: boolean * urlParams: boolean
* }} [sources] - A set/structure of {@code boolean} flags indicating the * }} [sources] - A set/structure of {@code boolean} flags indicating the
* configuration/preference/setting sources to consider/retrieve values from. * configuration/preference/setting sources to consider/retrieve values from.
@ -31,13 +33,13 @@ export function getPropertyValue(
// Defaults: // Defaults:
config: true, config: true,
jwt: true, jwt: true,
profile: true, settings: true,
urlParams: true, urlParams: true,
...sources ...sources
}; };
// Precedence: jwt -> urlParams -> profile -> config. // Precedence: jwt -> urlParams -> settings -> config.
const state = toState(stateful); const state = toState(stateful);
@ -61,9 +63,9 @@ export function getPropertyValue(
} }
} }
// profile // settings
if (sources.profile) { if (sources.settings) {
const value = state['features/base/profile'][propertyName]; const value = state['features/base/settings'][propertyName];
if (typeof value !== 'undefined') { if (typeof value !== 'undefined') {
return value; return value;

View File

@ -4,12 +4,13 @@ import { setAudioOnly } from '../conference';
import { getLocalParticipant, participantUpdated } from '../participants'; import { getLocalParticipant, participantUpdated } from '../participants';
import { MiddlewareRegistry, toState } from '../redux'; import { MiddlewareRegistry, toState } from '../redux';
import { PROFILE_UPDATED } from './actionTypes'; import { SETTINGS_UPDATED } from './actionTypes';
import { getSettings } from './functions';
/** /**
* The middleware of the feature base/profile. Distributes changes to the state * The middleware of the feature base/settings. Distributes changes to the state
* of base/profile to the states of other features computed from the state of * of base/settings to the states of other features computed from the state of
* base/profile. * base/settings.
* *
* @param {Store} store - The redux store. * @param {Store} store - The redux store.
* @returns {Function} * @returns {Function}
@ -18,16 +19,16 @@ MiddlewareRegistry.register(store => next => action => {
const result = next(action); const result = next(action);
switch (action.type) { switch (action.type) {
case PROFILE_UPDATED: case SETTINGS_UPDATED:
_updateLocalParticipant(store);
_maybeSetAudioOnly(store, action); _maybeSetAudioOnly(store, action);
_updateLocalParticipant(store);
} }
return result; return result;
}); });
/** /**
* Updates {@code startAudioOnly} flag if it's updated in the profile. * Updates {@code startAudioOnly} flag if it's updated in the settings.
* *
* @param {Store} store - The redux store. * @param {Store} store - The redux store.
* @param {Object} action - The redux action. * @param {Object} action - The redux action.
@ -36,14 +37,14 @@ MiddlewareRegistry.register(store => next => action => {
*/ */
function _maybeSetAudioOnly( function _maybeSetAudioOnly(
{ dispatch }, { dispatch },
{ profile: { startAudioOnly } }) { { settings: { startAudioOnly } }) {
if (typeof startAudioOnly === 'boolean') { if (typeof startAudioOnly === 'boolean') {
dispatch(setAudioOnly(startAudioOnly)); dispatch(setAudioOnly(startAudioOnly));
} }
} }
/** /**
* Updates the local participant according to profile changes. * Updates the local participant according to settings changes.
* *
* @param {Store} store - The redux store. * @param {Store} store - The redux store.
* @private * @private
@ -52,7 +53,7 @@ function _maybeSetAudioOnly(
function _updateLocalParticipant(store) { function _updateLocalParticipant(store) {
const state = toState(store); const state = toState(store);
const localParticipant = getLocalParticipant(state); const localParticipant = getLocalParticipant(state);
const profile = state['features/base/profile']; const settings = getSettings(state);
store.dispatch(participantUpdated({ store.dispatch(participantUpdated({
// Identify that the participant to update i.e. the local participant: // Identify that the participant to update i.e. the local participant:
@ -60,7 +61,7 @@ function _updateLocalParticipant(store) {
local: true, local: true,
// Specify the updates to be applied to the identified participant: // Specify the updates to be applied to the identified participant:
email: profile.email, email: settings.email,
name: profile.displayName name: settings.displayName
})); }));
} }

View File

@ -0,0 +1,157 @@
// @flow
import _ from 'lodash';
import { APP_WILL_MOUNT } from '../../app';
import JitsiMeetJS, { browser } from '../lib-jitsi-meet';
import { ReducerRegistry } from '../redux';
import { PersistenceRegistry } from '../storage';
import { assignIfDefined, randomHexString } from '../util';
import { SETTINGS_UPDATED } from './actionTypes';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* The default/initial redux state of the feature {@code base/settings}.
*
* @type Object
*/
const DEFAULT_STATE = {
audioOutputDeviceId: undefined,
avatarID: undefined,
avatarURL: undefined,
cameraDeviceId: undefined,
displayName: undefined,
email: undefined,
localFlipX: true,
micDeviceId: undefined,
serverURL: undefined,
startAudioOnly: false,
startWithAudioMuted: false,
startWithVideoMuted: false
};
const STORE_NAME = 'features/base/settings';
/**
* Sets up the persistence of the feature {@code base/settings}.
*/
PersistenceRegistry.register(STORE_NAME);
ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
switch (action.type) {
case APP_WILL_MOUNT:
return _initSettings(state);
case SETTINGS_UPDATED:
return {
...state,
...action.settings
};
}
return state;
});
/**
* Retrieves the legacy profile values regardless of it's being in pre or
* post-flattening format.
*
* FIXME: Let's remove this after a predefined time (e.g. by July 2018) to avoid
* garbage in the source.
*
* @private
* @returns {Object}
*/
function _getLegacyProfile() {
let persistedProfile
= window.localStorage.getItem('features/base/profile');
if (persistedProfile) {
try {
persistedProfile = JSON.parse(persistedProfile);
if (persistedProfile && typeof persistedProfile === 'object') {
const preFlattenedProfile = persistedProfile.profile;
return preFlattenedProfile || persistedProfile;
}
} catch (e) {
logger.warn('Error parsing persisted legacy profile', e);
}
}
return {};
}
/**
* Inits the settings object based on what information we have available.
* Info taken into consideration:
* - Old Settings.js style data
* - Things that we stored in profile earlier but belong here.
*
* @private
* @param {Object} featureState - The current state of the feature.
* @returns {Object}
*/
function _initSettings(featureState) {
let settings = featureState;
// Old Settings.js values
// FIXME: Let's remove this after a predefined time (e.g. by July 2018) to
// avoid garbage in the source.
const displayName = _.escape(window.localStorage.getItem('displayname'));
const email = _.escape(window.localStorage.getItem('email'));
let avatarID = _.escape(window.localStorage.getItem('avatarId'));
if (!avatarID) {
// if there is no avatar id, we generate a unique one and use it forever
avatarID = randomHexString(32);
}
settings = assignIfDefined({
avatarID,
displayName,
email
}, settings);
if (!browser.isReactNative()) {
// Browser only
const localFlipX
= JSON.parse(window.localStorage.getItem('localFlipX') || 'true');
const cameraDeviceId
= window.localStorage.getItem('cameraDeviceId') || '';
const micDeviceId = window.localStorage.getItem('micDeviceId') || '';
// Currently audio output device change is supported only in Chrome and
// default output always has 'default' device ID
const audioOutputDeviceId
= window.localStorage.getItem('audioOutputDeviceId') || 'default';
if (audioOutputDeviceId
!== JitsiMeetJS.mediaDevices.getAudioOutputDevice()) {
JitsiMeetJS.mediaDevices.setAudioOutputDevice(
audioOutputDeviceId
).catch(ex => {
logger.warn('Failed to set audio output device from local '
+ 'storage. Default audio output device will be used'
+ 'instead.', ex);
});
}
settings = assignIfDefined({
audioOutputDeviceId,
cameraDeviceId,
localFlipX,
micDeviceId
}, settings);
}
// Things we stored in profile earlier
const legacyProfile = _getLegacyProfile();
settings = assignIfDefined(legacyProfile, settings);
return settings;
}

View File

@ -6,18 +6,20 @@ store/state into window.localStorage (on Web) or AsyncStorage (on mobile).
Usage Usage
===== =====
If a subtree of the redux store should be persisted (e.g. If a subtree of the redux store should be persisted (e.g.
`'features/base/profile'`), then persistence for that subtree should be `'features/base/settings'`), then persistence for that subtree should be
requested by registering the subtree with `PersistenceRegistry`. requested by registering the subtree with `PersistenceRegistry`.
For example, to register the field `profile` of the redux subtree For example, to register the field `displayName` of the redux subtree
`'features/base/profile'` to be persisted, use: `'features/base/settings'` to be persisted, use:
```javascript ```javascript
PersistenceRegistry.register('features/base/profile', { PersistenceRegistry.register('features/base/settings', {
profile: true displayName: true
}); });
``` ```
in the `reducer.js` of the `base/profile` feature. in the `reducer.js` of the `base/settings` feature.
If the second parameter is omitted, the entire feature state is persisted.
When it's done, Jitsi Meet will automatically persist these subtrees and When it's done, Jitsi Meet will automatically persist these subtrees and
rehydrate them on startup. rehydrate them on startup.
@ -31,3 +33,12 @@ throttling timeout can be configured in
``` ```
react/features/base/storage/middleware.js#PERSIST_STATE_DELAY react/features/base/storage/middleware.js#PERSIST_STATE_DELAY
``` ```
Serialization
=============
The API JSON.stringify() is currently used to serialize feature states,
therefore its limitations affect the persistency feature too. E.g. complex
objects, such as Maps (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
or Sets (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set)
cannot be automatically persisted at the moment. The same applies to Functions
(which is not a good practice to store in Redux anyhow).

View File

@ -35,14 +35,16 @@ export function createLocalTracksF(
if (typeof APP !== 'undefined') { if (typeof APP !== 'undefined') {
// TODO The app's settings should go in the redux store and then the // TODO The app's settings should go in the redux store and then the
// reliance on the global variable APP will go away. // reliance on the global variable APP will go away.
store || (store = APP.store); // eslint-disable-line no-param-reassign
const settings = store.getState()['features/base/settings'];
if (typeof cameraDeviceId === 'undefined' || cameraDeviceId === null) { if (typeof cameraDeviceId === 'undefined' || cameraDeviceId === null) {
cameraDeviceId = APP.settings.getCameraDeviceId(); cameraDeviceId = settings.cameraDeviceId;
} }
if (typeof micDeviceId === 'undefined' || micDeviceId === null) { if (typeof micDeviceId === 'undefined' || micDeviceId === null) {
micDeviceId = APP.settings.getMicDeviceId(); micDeviceId = settings.micDeviceId;
} }
store || (store = APP.store); // eslint-disable-line no-param-reassign
} }
const { const {

View File

@ -18,3 +18,27 @@ export function getJitsiMeetGlobalNS() {
return window.JitsiMeetJS.app; return window.JitsiMeetJS.app;
} }
/**
* A helper function that behaves similar to Object.assign, but only reassigns a
* property in target if it's defined in source.
*
* @param {Object} target - The target object to assign the values into.
* @param {Object} source - The source object.
* @returns {Object}
*/
export function assignIfDefined(target: Object, source: Object) {
const to = Object(target);
for (const nextKey in source) {
if (source.hasOwnProperty(nextKey)) {
const value = source[nextKey];
if (typeof value !== 'undefined') {
to[nextKey] = value;
}
}
}
return to;
}

View File

@ -1,19 +1,21 @@
/* globals APP, interfaceConfig */ /* globals APP, interfaceConfig */
import { openDialog } from '../base/dialog';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { API_ID } from '../../../modules/API/constants'; import { API_ID } from '../../../modules/API/constants';
import {
setAudioInputDevice,
setAudioOutputDevice,
setVideoInputDevice
} from '../base/devices';
import { i18next } from '../base/i18n';
import { import {
PostMessageTransportBackend, PostMessageTransportBackend,
Transport Transport
} from '../../../modules/transport'; } from '../../../modules/transport';
import {
getAudioOutputDeviceId,
setAudioInputDevice,
setAudioOutputDevice,
setVideoInputDevice
} from '../base/devices';
import { openDialog } from '../base/dialog';
import { i18next } from '../base/i18n';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes'; import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
import { DeviceSelectionDialog } from './components'; import { DeviceSelectionDialog } from './components';
@ -42,10 +44,12 @@ function _openDeviceSelectionDialogHere() {
return dispatch => return dispatch =>
JitsiMeetJS.mediaDevices.isDeviceListAvailable() JitsiMeetJS.mediaDevices.isDeviceListAvailable()
.then(isDeviceListAvailable => { .then(isDeviceListAvailable => {
const settings = APP.store.getState()['features/base/settings'];
dispatch(openDialog(DeviceSelectionDialog, { dispatch(openDialog(DeviceSelectionDialog, {
currentAudioInputId: APP.settings.getMicDeviceId(), currentAudioInputId: settings.micDeviceId,
currentAudioOutputId: APP.settings.getAudioOutputDeviceId(), currentAudioOutputId: getAudioOutputDeviceId(),
currentVideoInputId: APP.settings.getCameraDeviceId(), currentVideoInputId: settings.cameraDeviceId,
disableAudioInputChange: disableAudioInputChange:
!JitsiMeetJS.isMultipleAudioInputSupported(), !JitsiMeetJS.isMultipleAudioInputSupported(),
disableDeviceChange: !isDeviceListAvailable disableDeviceChange: !isDeviceListAvailable
@ -135,6 +139,9 @@ function _openDeviceSelectionDialogInPopup() {
*/ */
function _processRequest(dispatch, getState, request, responseCallback) { // eslint-disable-line max-len, max-params function _processRequest(dispatch, getState, request, responseCallback) { // eslint-disable-line max-len, max-params
if (request.type === 'devices') { if (request.type === 'devices') {
const state = getState();
const settings = state['features/base/settings'];
switch (request.name) { switch (request.name) {
case 'isDeviceListAvailable': case 'isDeviceListAvailable':
JitsiMeetJS.mediaDevices.isDeviceListAvailable() JitsiMeetJS.mediaDevices.isDeviceListAvailable()
@ -152,9 +159,9 @@ function _processRequest(dispatch, getState, request, responseCallback) { // esl
break; break;
case 'getCurrentDevices': case 'getCurrentDevices':
responseCallback({ responseCallback({
audioInput: APP.settings.getMicDeviceId(), audioInput: settings.micDeviceId,
audioOutput: APP.settings.getAudioOutputDeviceId(), audioOutput: getAudioOutputDeviceId(),
videoInput: APP.settings.getCameraDeviceId() videoInput: settings.cameraDeviceId
}); });
break; break;
case 'getAvailableDevices': case 'getAvailableDevices':

View File

@ -2,7 +2,7 @@
import { Component } from 'react'; import { Component } from 'react';
import { updateProfile } from '../../base/profile'; import { updateSettings } from '../../base/settings';
/** /**
* The type of the React {@code Component} props of * The type of the React {@code Component} props of
@ -11,19 +11,17 @@ import { updateProfile } from '../../base/profile';
type Props = { type Props = {
/** /**
* The current profile object. * The default URL for when there is no custom URL set in the settings.
*
* @protected
*/
_profile: Object,
/**
* The default URL for when there is no custom URL set in the profile.
* *
* @protected * @protected
*/ */
_serverURL: string, _serverURL: string,
/**
* The current settings object.
*/
_settings: Object,
/** /**
* Whether {@link AbstractSettingsView} is visible. * Whether {@link AbstractSettingsView} is visible.
* *
@ -79,7 +77,7 @@ export class AbstractSettingsView extends Component<Props> {
* @returns {void} * @returns {void}
*/ */
_onChangeDisplayName(text) { _onChangeDisplayName(text) {
this._updateProfile({ this._updateSettings({
displayName: text displayName: text
}); });
} }
@ -94,7 +92,7 @@ export class AbstractSettingsView extends Component<Props> {
* @returns {void} * @returns {void}
*/ */
_onChangeEmail(text) { _onChangeEmail(text) {
this._updateProfile({ this._updateSettings({
email: text email: text
}); });
} }
@ -109,7 +107,7 @@ export class AbstractSettingsView extends Component<Props> {
* @returns {void} * @returns {void}
*/ */
_onChangeServerURL(text) { _onChangeServerURL(text) {
this._updateProfile({ this._updateSettings({
serverURL: text serverURL: text
}); });
} }
@ -125,7 +123,7 @@ export class AbstractSettingsView extends Component<Props> {
* @returns {void} * @returns {void}
*/ */
_onStartAudioMutedChange(newValue) { _onStartAudioMutedChange(newValue) {
this._updateProfile({ this._updateSettings({
startWithAudioMuted: newValue startWithAudioMuted: newValue
}); });
} }
@ -141,22 +139,23 @@ export class AbstractSettingsView extends Component<Props> {
* @returns {void} * @returns {void}
*/ */
_onStartVideoMutedChange(newValue) { _onStartVideoMutedChange(newValue) {
this._updateProfile({ this._updateSettings({
startWithVideoMuted: newValue startWithVideoMuted: newValue
}); });
} }
_updateProfile: (Object) => void; _updateSettings: (Object) => void;
/** /**
* Updates the persisted profile on any change. * Updates the persisted settings on any change.
* *
* @param {Object} updateObject - The partial update object for the profile. * @param {Object} updateObject - The partial update object for the
* settings.
* @private * @private
* @returns {void} * @returns {void}
*/ */
_updateProfile(updateObject: Object) { _updateSettings(updateObject: Object) {
this.props.dispatch(updateProfile(updateObject)); this.props.dispatch(updateSettings(updateObject));
} }
} }
@ -167,15 +166,15 @@ export class AbstractSettingsView extends Component<Props> {
* @param {Object} state - The redux state. * @param {Object} state - The redux state.
* @protected * @protected
* @returns {{ * @returns {{
* _profile: Object,
* _serverURL: string, * _serverURL: string,
* _settings: Object,
* _visible: boolean * _visible: boolean
* }} * }}
*/ */
export function _mapStateToProps(state: Object) { export function _mapStateToProps(state: Object) {
return { return {
_profile: state['features/base/profile'],
_serverURL: state['features/app'].app._getDefaultURL(), _serverURL: state['features/app'].app._getDefaultURL(),
_settings: state['features/base/settings'],
_visible: state['features/settings'].visible _visible: state['features/settings'].visible
}; };
} }

View File

@ -120,7 +120,7 @@ class SettingsView extends AbstractSettingsView {
* @returns {void} * @returns {void}
*/ */
_processServerURL(hideOnSuccess: boolean) { _processServerURL(hideOnSuccess: boolean) {
const { serverURL } = this.props._profile; const { serverURL } = this.props._settings;
const normalizedURL = normalizeUserInputURL(serverURL); const normalizedURL = normalizeUserInputURL(serverURL);
if (normalizedURL === null) { if (normalizedURL === null) {
@ -140,7 +140,7 @@ class SettingsView extends AbstractSettingsView {
* @returns {React$Element} * @returns {React$Element}
*/ */
_renderBody() { _renderBody() {
const { _profile } = this.props; const { _settings } = this.props;
return ( return (
<SafeAreaView style = { styles.settingsForm }> <SafeAreaView style = { styles.settingsForm }>
@ -154,7 +154,7 @@ class SettingsView extends AbstractSettingsView {
autoCorrect = { false } autoCorrect = { false }
onChangeText = { this._onChangeDisplayName } onChangeText = { this._onChangeDisplayName }
placeholder = 'John Doe' placeholder = 'John Doe'
value = { _profile.displayName } /> value = { _settings.displayName } />
</FormRow> </FormRow>
<FormRow i18nLabel = 'settingsView.email'> <FormRow i18nLabel = 'settingsView.email'>
<TextInput <TextInput
@ -163,7 +163,7 @@ class SettingsView extends AbstractSettingsView {
keyboardType = { 'email-address' } keyboardType = { 'email-address' }
onChangeText = { this._onChangeEmail } onChangeText = { this._onChangeEmail }
placeholder = 'email@example.com' placeholder = 'email@example.com'
value = { _profile.email } /> value = { _settings.email } />
</FormRow> </FormRow>
<FormSectionHeader <FormSectionHeader
i18nLabel = 'settingsView.conferenceSection' /> i18nLabel = 'settingsView.conferenceSection' />
@ -176,19 +176,19 @@ class SettingsView extends AbstractSettingsView {
onBlur = { this._onBlurServerURL } onBlur = { this._onBlurServerURL }
onChangeText = { this._onChangeServerURL } onChangeText = { this._onChangeServerURL }
placeholder = { this.props._serverURL } placeholder = { this.props._serverURL }
value = { _profile.serverURL } /> value = { _settings.serverURL } />
</FormRow> </FormRow>
<FormRow <FormRow
fieldSeparator = { true } fieldSeparator = { true }
i18nLabel = 'settingsView.startWithAudioMuted'> i18nLabel = 'settingsView.startWithAudioMuted'>
<Switch <Switch
onValueChange = { this._onStartAudioMutedChange } onValueChange = { this._onStartAudioMutedChange }
value = { _profile.startWithAudioMuted } /> value = { _settings.startWithAudioMuted } />
</FormRow> </FormRow>
<FormRow i18nLabel = 'settingsView.startWithVideoMuted'> <FormRow i18nLabel = 'settingsView.startWithVideoMuted'>
<Switch <Switch
onValueChange = { this._onStartVideoMutedChange } onValueChange = { this._onStartVideoMutedChange }
value = { _profile.startWithVideoMuted } /> value = { _settings.startWithVideoMuted } />
</FormRow> </FormRow>
</ScrollView> </ScrollView>
</SafeAreaView> </SafeAreaView>

View File

@ -14,10 +14,18 @@ import { generateRoomWithoutSeparator } from '../functions';
type Props = { type Props = {
/** /**
* The user's profile. * Room name to join to.
*/ */
_profile: Object,
_room: string, _room: string,
/**
* The current settings.
*/
_settings: Object,
/**
* The Redux dispatch Function.
*/
dispatch: Dispatch<*> dispatch: Dispatch<*>
}; };
@ -229,13 +237,13 @@ export class AbstractWelcomePage extends Component<Props, *> {
* @param {Object} state - The redux state. * @param {Object} state - The redux state.
* @protected * @protected
* @returns {{ * @returns {{
* _profile: Object, * _room: string,
* _room: string * _settings: Object
* }} * }}
*/ */
export function _mapStateToProps(state: Object) { export function _mapStateToProps(state: Object) {
return { return {
_profile: state['features/base/profile'], _room: state['features/base/conference'].room,
_room: state['features/base/conference'].room _settings: state['features/base/settings']
}; };
} }

View File

@ -5,8 +5,8 @@ import { Switch, TouchableWithoutFeedback, View } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { updateProfile } from '../../base/profile';
import { Header, Text } from '../../base/react'; import { Header, Text } from '../../base/react';
import { updateSettings } from '../../base/settings';
import styles, { SWITCH_THUMB_COLOR, SWITCH_UNDER_COLOR } from './styles'; import styles, { SWITCH_THUMB_COLOR, SWITCH_UNDER_COLOR } from './styles';
@ -26,9 +26,9 @@ type Props = {
t: Function, t: Function,
/** /**
* The current profile settings from redux. * The current settings from redux.
*/ */
_profile: Object _settings: Object
}; };
/** /**
@ -55,7 +55,7 @@ class VideoSwitch extends Component<Props> {
* @inheritdoc * @inheritdoc
*/ */
render() { render() {
const { t, _profile } = this.props; const { t, _settings } = this.props;
const { textStyle } = Header; const { textStyle } = Header;
return ( return (
@ -71,7 +71,7 @@ class VideoSwitch extends Component<Props> {
onValueChange = { this._onStartAudioOnlyChange } onValueChange = { this._onStartAudioOnlyChange }
style = { styles.audioVideoSwitch } style = { styles.audioVideoSwitch }
thumbTintColor = { SWITCH_THUMB_COLOR } thumbTintColor = { SWITCH_THUMB_COLOR }
value = { _profile.startAudioOnly } /> value = { _settings.startAudioOnly } />
<TouchableWithoutFeedback <TouchableWithoutFeedback
onPress = { this._onStartAudioOnlyTrue }> onPress = { this._onStartAudioOnlyTrue }>
<Text style = { textStyle }> <Text style = { textStyle }>
@ -94,8 +94,7 @@ class VideoSwitch extends Component<Props> {
_onStartAudioOnlyChange(startAudioOnly) { _onStartAudioOnlyChange(startAudioOnly) {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch(updateProfile({ dispatch(updateSettings({
...this.props._profile,
startAudioOnly startAudioOnly
})); }));
} }
@ -124,12 +123,12 @@ class VideoSwitch extends Component<Props> {
* @param {Object} state - The redux state. * @param {Object} state - The redux state.
* @protected * @protected
* @returns {{ * @returns {{
* _profile: Object * _settings: Object
* }} * }}
*/ */
export function _mapStateToProps(state: Object) { export function _mapStateToProps(state: Object) {
return { return {
_profile: state['features/base/profile'] _settings: state['features/base/settings']
}; };
} }

View File

@ -66,7 +66,7 @@ class WelcomePage extends AbstractWelcomePage {
const { dispatch } = this.props; const { dispatch } = this.props;
if (this.props._profile.startAudioOnly) { if (this.props._settings.startAudioOnly) {
dispatch(destroyLocalTracks()); dispatch(destroyLocalTracks());
} else { } else {
dispatch(createDesiredLocalTracks(MEDIA_TYPE.VIDEO)); dispatch(createDesiredLocalTracks(MEDIA_TYPE.VIDEO));