diff --git a/app.js b/app.js index 6280e66ee..03c9fe35b 100644 --- a/app.js +++ b/app.js @@ -15,7 +15,6 @@ import conference from './conference'; import API from './modules/API'; import keyboardshortcut from './modules/keyboardshortcut/keyboardshortcut'; import remoteControl from './modules/remotecontrol/RemoteControl'; -import settings from './modules/settings/Settings'; import translation from './modules/translation/translation'; import UI from './modules/UI/UI'; @@ -41,7 +40,6 @@ window.APP = { keyboardshortcut, remoteControl, - settings, translation, UI }; diff --git a/conference.js b/conference.js index 7cf1673fa..9c8c762c7 100644 --- a/conference.js +++ b/conference.js @@ -46,7 +46,10 @@ import { sendLocalParticipant, setDesktopSharingEnabled } from './react/features/base/conference'; -import { updateDeviceList } from './react/features/base/devices'; +import { + setAudioOutputDeviceId, + updateDeviceList +} from './react/features/base/devices'; import { isFatalJitsiConnectionError, JitsiConferenceErrors, @@ -82,6 +85,7 @@ import { participantRoleChanged, participantUpdated } from './react/features/base/participants'; +import { updateSettings } from './react/features/base/settings'; import { createLocalTracksF, isLocalTrackMuted, @@ -1273,7 +1277,7 @@ export default { : 'colibri'; } - const nick = APP.settings.getDisplayName(); + const nick = APP.store.getState()['features/base/settings'].displayName; if (nick) { options.displayName = nick; @@ -2131,7 +2135,9 @@ export default { }) .then(() => { logger.log('switched local video device'); - APP.settings.setCameraDeviceId(cameraDeviceId, true); + APP.store.dispatch(updateSettings({ + cameraDeviceId + })); }) .catch(err => { APP.UI.showCameraErrorNotification(err); @@ -2163,7 +2169,9 @@ export default { .then(stream => { this.useAudioStream(stream); logger.log('switched local audio device'); - APP.settings.setMicDeviceId(micDeviceId, true); + APP.store.dispatch(updateSettings({ + micDeviceId + })); }) .catch(err => { APP.UI.showMicErrorNotification(err); @@ -2175,7 +2183,7 @@ export default { UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED, audioOutputDeviceId => { sendAnalytics(createDeviceChangedEvent('audio', 'output')); - APP.settings.setAudioOutputDeviceId(audioOutputDeviceId) + setAudioOutputDeviceId(audioOutputDeviceId) .then(() => logger.log('changed audio output device')) .catch(err => { logger.warn('Failed to change audio output device. ' @@ -2317,7 +2325,8 @@ export default { APP.store.dispatch(conferenceJoined(room)); APP.UI.mucJoined(); - const displayName = APP.settings.getDisplayName(); + const displayName + = APP.store.getState()['features/base/settings'].displayName; APP.API.notifyConferenceJoined( this.roomName, @@ -2367,14 +2376,18 @@ export default { // storage and settings menu. This is a workaround until // getConstraints() method will be implemented // in browsers. + const { dispatch } = APP.store; + if (this.localAudio) { - APP.settings.setMicDeviceId( - this.localAudio.getDeviceId(), false); + dispatch(updateSettings({ + micDeviceId: this.localAudio.getDeviceId() + })); } if (this.localVideo) { - APP.settings.setCameraDeviceId( - this.localVideo.getDeviceId(), false); + dispatch(updateSettings({ + cameraDeviceId: this.localVideo.getDeviceId() + })); } mediaDeviceHelper.setCurrentMediaDevices(devices); @@ -2426,8 +2439,7 @@ export default { if (typeof newDevices.audiooutput !== 'undefined') { // Just ignore any errors in catch block. - promises.push(APP.settings - .setAudioOutputDeviceId(newDevices.audiooutput) + promises.push(setAudioOutputDeviceId(newDevices.audiooutput) .catch()); } @@ -2576,7 +2588,10 @@ export default { email: formattedEmail })); - APP.settings.setEmail(formattedEmail); + APP.store.dispatch(updateSettings({ + email: formattedEmail + })); + APP.UI.setUserEmail(localId, formattedEmail); sendData(commands.EMAIL, formattedEmail); }, @@ -2600,7 +2615,9 @@ export default { avatarURL: formattedUrl })); - APP.settings.setAvatarUrl(url); + APP.store.dispatch(updateSettings({ + avatarURL: formattedUrl + })); sendData(commands.AVATAR_URL, url); }, @@ -2654,7 +2671,10 @@ export default { name: formattedNickname })); - APP.settings.setDisplayName(formattedNickname); + APP.store.dispatch(updateSettings({ + displayName: formattedNickname + })); + APP.API.notifyDisplayNameChanged(id, { displayName: formattedNickname, formattedDisplayName: diff --git a/modules/UI/side_pannels/profile/Profile.js b/modules/UI/side_pannels/profile/Profile.js index 439ee0340..0fd6897d6 100644 --- a/modules/UI/side_pannels/profile/Profile.js +++ b/modules/UI/side_pannels/profile/Profile.js @@ -1,7 +1,6 @@ /* global $, APP */ import UIUtil from '../../util/UIUtil'; import UIEvents from '../../../../service/UI/UIEvents'; -import Settings from '../../../settings/Settings'; import { createProfilePanelButtonEvent, @@ -54,6 +53,8 @@ export default { init(emitter) { initHTML(); + const settings = APP.store.getState()['features/base/settings']; + /** * Updates display name. * @@ -64,7 +65,7 @@ export default { } $('#setDisplayName') - .val(Settings.getDisplayName()) + .val(settings.displayName) .keyup(event => { if (event.keyCode === 13) { // enter updateDisplayName(); @@ -82,7 +83,7 @@ export default { } $('#setEmail') - .val(Settings.getEmail()) + .val(settings.email) .keyup(event => { if (event.keyCode === 13) { // enter updateEmail(); diff --git a/modules/UI/util/UIUtil.js b/modules/UI/util/UIUtil.js index c070a8c25..cc7d44309 100644 --- a/modules/UI/util/UIUtil.js +++ b/modules/UI/util/UIUtil.js @@ -74,17 +74,6 @@ const UIUtil = { .html(); }, - /** - * Unescapes the given text. - * - * @param {string} safe string which contains escaped html - * @returns {string} unescaped html string. - */ - unescapeHtml(safe) { - return $('
').html(safe) - .text(); - }, - imageToGrayScale(canvas) { const context = canvas.getContext('2d'); const imgData = context.getImageData(0, 0, canvas.width, canvas.height); diff --git a/modules/UI/videolayout/LocalVideo.js b/modules/UI/videolayout/LocalVideo.js index 86f86b146..f9ba8e593 100644 --- a/modules/UI/videolayout/LocalVideo.js +++ b/modules/UI/videolayout/LocalVideo.js @@ -10,6 +10,7 @@ import { VideoTrack } from '../../../react/features/base/media'; import { getAvatarURLByParticipantId } from '../../../react/features/base/participants'; +import { updateSettings } from '../../../react/features/base/settings'; /* eslint-enable no-unused-vars */ const logger = require('jitsi-meet-logger').getLogger(__filename); @@ -121,9 +122,10 @@ LocalVideo.prototype.changeVideo = function(stream) { // eslint-disable-next-line eqeqeq const isVideo = stream.videoType != 'desktop'; + const settings = APP.store.getState()['features/base/settings']; this._enableDisableContextMenu(isVideo); - this.setFlipX(isVideo ? APP.settings.getLocalFlipX() : false); + this.setFlipX(isVideo ? settings.localFlipX : false); const endedHandler = () => { @@ -194,10 +196,14 @@ LocalVideo.prototype._buildContextMenu = function() { flip: { name: 'Flip', callback: () => { - const val = !APP.settings.getLocalFlipX(); + const { store } = APP; + const val = !store.getState()['features/base/settings'] + .localFlipX; this.setFlipX(val); - APP.settings.setLocalFlipX(val); + store.dispatch(updateSettings({ + localFlipX: val + })); } } }, diff --git a/modules/devices/mediaDeviceHelper.js b/modules/devices/mediaDeviceHelper.js index 787ddfd67..7b8e4500b 100644 --- a/modules/devices/mediaDeviceHelper.js +++ b/modules/devices/mediaDeviceHelper.js @@ -1,5 +1,7 @@ /* global APP, JitsiMeetJS */ +import { getAudioOutputDeviceId } from '../../react/features/base/devices'; + let currentAudioInputDevices, currentAudioOutputDevices, currentVideoInputDevices; @@ -16,7 +18,7 @@ function getNewAudioOutputDevice(newDevices) { return; } - const selectedAudioOutputDeviceId = APP.settings.getAudioOutputDeviceId(); + const selectedAudioOutputDeviceId = getAudioOutputDeviceId(); const availableAudioOutputDevices = newDevices.filter( d => d.kind === 'audiooutput'); @@ -40,7 +42,8 @@ function getNewAudioOutputDevice(newDevices) { function getNewAudioInputDevice(newDevices, localAudio) { const availableAudioInputDevices = newDevices.filter( 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( d => d.deviceId === selectedAudioInputDeviceId); @@ -76,7 +79,8 @@ function getNewAudioInputDevice(newDevices, localAudio) { function getNewVideoInputDevice(newDevices, localVideo) { const availableVideoInputDevices = newDevices.filter( 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( d => d.deviceId === selectedVideoInputDeviceId); diff --git a/modules/settings/Settings.js b/modules/settings/Settings.js deleted file mode 100644 index f0f745f5f..000000000 --- a/modules/settings/Settings.js +++ /dev/null @@ -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)); - } -}; diff --git a/react/features/app/components/AbstractApp.js b/react/features/app/components/AbstractApp.js index 7ed305f96..ea1235854 100644 --- a/react/features/app/components/AbstractApp.js +++ b/react/features/app/components/AbstractApp.js @@ -12,7 +12,6 @@ import { localParticipantJoined, localParticipantLeft } from '../../base/participants'; -import '../../base/profile'; import { Fragment, RouteRegistry } from '../../base/react'; import { MiddlewareRegistry, ReducerRegistry } from '../../base/redux'; 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} * 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'; @@ -133,26 +135,14 @@ export class AbstractApp extends Component { // actions is important, not the call site. Moreover, we've got // localParticipant business logic in the React Component // (i.e. UI) AbstractApp now. - let localParticipant = {}; - if (typeof APP === 'object') { - localParticipant = { - avatarID: APP.settings.getAvatarId(), - avatarURL: APP.settings.getAvatarUrl(), - email: APP.settings.getEmail(), - name: APP.settings.getDisplayName() - }; - } - - // 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; - } + const settings = getState()['features/base/settings']; + const localParticipant = { + avatarID: settings.avatarID, + avatarURL: settings.avatarURL, + email: settings.email, + name: settings.displayName + }; // We set the initialized state here and not in the contructor to // make sure that {@code componentWillMount} gets invoked before @@ -383,8 +373,8 @@ export class AbstractApp extends Component { return ( this.props.defaultURL - || this._getStore().getState()['features/base/profile'] - .serverURL + || this._getStore().getState()['features/base/settings'] + .serverURL || DEFAULT_URL); } diff --git a/react/features/base/devices/functions.js b/react/features/base/devices/functions.js new file mode 100644 index 000000000..c5d2b2739 --- /dev/null +++ b/react/features/base/devices/functions.js @@ -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 + }))); +} diff --git a/react/features/base/devices/index.js b/react/features/base/devices/index.js index f3a2aac65..969667b3a 100644 --- a/react/features/base/devices/index.js +++ b/react/features/base/devices/index.js @@ -1,5 +1,6 @@ export * from './actions'; export * from './actionTypes'; +export * from './functions'; import './middleware'; import './reducer'; diff --git a/react/features/base/media/middleware.js b/react/features/base/media/middleware.js index d8e4ba664..a1880d73e 100644 --- a/react/features/base/media/middleware.js +++ b/react/features/base/media/middleware.js @@ -8,8 +8,8 @@ import { } from '../../analytics'; import { isRoomValid, SET_ROOM, setAudioOnly } from '../conference'; import JitsiMeetJS from '../lib-jitsi-meet'; -import { getPropertyValue } from '../profile'; import { MiddlewareRegistry } from '../redux'; +import { getPropertyValue } from '../settings'; import { setTrackMuted, TRACK_ADDED } from '../tracks'; import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions'; @@ -79,11 +79,11 @@ function _setRoom({ dispatch, getState }, next, action) { const mutedSources = { // We have startWithAudioMuted and startWithVideoMuted here: config: true, - profile: true, + settings: true, // XXX We've already overwritten base/config with urlParams. However, - // profile is more important than the server-side config. Consequently, - // we need to read from urlParams anyway: + // settings are more important than the server-side config. + // Consequently, we need to read from urlParams anyway: urlParams: true, // We don't have startWithAudioMuted and startWithVideoMuted here: @@ -141,7 +141,7 @@ function _setRoom({ dispatch, getState }, next, action) { config: roomIsValid, // 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, // we need to read from urlParams anyway. We also // 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 // they are defined or not: jwt: false, - profile: true + settings: true })); } else { // Default to audio-only if the (execution) environment does not diff --git a/react/features/base/profile/actionTypes.js b/react/features/base/profile/actionTypes.js deleted file mode 100644 index 2301693e3..000000000 --- a/react/features/base/profile/actionTypes.js +++ /dev/null @@ -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'); diff --git a/react/features/base/profile/actions.js b/react/features/base/profile/actions.js deleted file mode 100644 index da7e1d60b..000000000 --- a/react/features/base/profile/actions.js +++ /dev/null @@ -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 - }; -} diff --git a/react/features/base/profile/reducer.js b/react/features/base/profile/reducer.js deleted file mode 100644 index 0d8553faa..000000000 --- a/react/features/base/profile/reducer.js +++ /dev/null @@ -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; -}); diff --git a/react/features/base/settings/actionTypes.js b/react/features/base/settings/actionTypes.js new file mode 100644 index 000000000..bce7fde7a --- /dev/null +++ b/react/features/base/settings/actionTypes.js @@ -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'); diff --git a/react/features/base/settings/actions.js b/react/features/base/settings/actions.js new file mode 100644 index 000000000..5f138730f --- /dev/null +++ b/react/features/base/settings/actions.js @@ -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 + }; +} diff --git a/react/features/base/profile/functions.js b/react/features/base/settings/functions.js similarity index 86% rename from react/features/base/profile/functions.js rename to react/features/base/settings/functions.js index 7c6c40bff..62326a352 100644 --- a/react/features/base/profile/functions.js +++ b/react/features/base/settings/functions.js @@ -3,9 +3,11 @@ import { parseURLParams } from '../config'; import { toState } from '../redux'; + /** * 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 * {@code getState} function. @@ -14,7 +16,7 @@ import { toState } from '../redux'; * @param {{ * config: boolean, * jwt: boolean, - * profile: boolean, + * settings: boolean, * urlParams: boolean * }} [sources] - A set/structure of {@code boolean} flags indicating the * configuration/preference/setting sources to consider/retrieve values from. @@ -31,13 +33,13 @@ export function getPropertyValue( // Defaults: config: true, jwt: true, - profile: true, + settings: true, urlParams: true, ...sources }; - // Precedence: jwt -> urlParams -> profile -> config. + // Precedence: jwt -> urlParams -> settings -> config. const state = toState(stateful); @@ -61,9 +63,9 @@ export function getPropertyValue( } } - // profile - if (sources.profile) { - const value = state['features/base/profile'][propertyName]; + // settings + if (sources.settings) { + const value = state['features/base/settings'][propertyName]; if (typeof value !== 'undefined') { return value; diff --git a/react/features/base/profile/index.js b/react/features/base/settings/index.js similarity index 100% rename from react/features/base/profile/index.js rename to react/features/base/settings/index.js diff --git a/react/features/base/profile/middleware.js b/react/features/base/settings/middleware.js similarity index 68% rename from react/features/base/profile/middleware.js rename to react/features/base/settings/middleware.js index 99d38efac..d019fdbee 100644 --- a/react/features/base/profile/middleware.js +++ b/react/features/base/settings/middleware.js @@ -4,12 +4,13 @@ import { setAudioOnly } from '../conference'; import { getLocalParticipant, participantUpdated } from '../participants'; 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 - * of base/profile to the states of other features computed from the state of - * base/profile. + * The middleware of the feature base/settings. Distributes changes to the state + * of base/settings to the states of other features computed from the state of + * base/settings. * * @param {Store} store - The redux store. * @returns {Function} @@ -18,16 +19,16 @@ MiddlewareRegistry.register(store => next => action => { const result = next(action); switch (action.type) { - case PROFILE_UPDATED: - _updateLocalParticipant(store); + case SETTINGS_UPDATED: _maybeSetAudioOnly(store, action); + _updateLocalParticipant(store); } 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 {Object} action - The redux action. @@ -36,14 +37,14 @@ MiddlewareRegistry.register(store => next => action => { */ function _maybeSetAudioOnly( { dispatch }, - { profile: { startAudioOnly } }) { + { settings: { startAudioOnly } }) { if (typeof startAudioOnly === 'boolean') { 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. * @private @@ -52,7 +53,7 @@ function _maybeSetAudioOnly( function _updateLocalParticipant(store) { const state = toState(store); const localParticipant = getLocalParticipant(state); - const profile = state['features/base/profile']; + const settings = getSettings(state); store.dispatch(participantUpdated({ // Identify that the participant to update i.e. the local participant: @@ -60,7 +61,7 @@ function _updateLocalParticipant(store) { local: true, // Specify the updates to be applied to the identified participant: - email: profile.email, - name: profile.displayName + email: settings.email, + name: settings.displayName })); } diff --git a/react/features/base/settings/reducer.js b/react/features/base/settings/reducer.js new file mode 100644 index 000000000..529a4b8aa --- /dev/null +++ b/react/features/base/settings/reducer.js @@ -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; +} diff --git a/react/features/base/storage/README.md b/react/features/base/storage/README.md index 6d1d4209b..599d23aca 100644 --- a/react/features/base/storage/README.md +++ b/react/features/base/storage/README.md @@ -6,18 +6,20 @@ store/state into window.localStorage (on Web) or AsyncStorage (on mobile). Usage ===== 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`. -For example, to register the field `profile` of the redux subtree -`'features/base/profile'` to be persisted, use: +For example, to register the field `displayName` of the redux subtree +`'features/base/settings'` to be persisted, use: ```javascript -PersistenceRegistry.register('features/base/profile', { - profile: true +PersistenceRegistry.register('features/base/settings', { + 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 rehydrate them on startup. @@ -31,3 +33,12 @@ throttling timeout can be configured in ``` 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). diff --git a/react/features/base/tracks/functions.js b/react/features/base/tracks/functions.js index df18f3d1d..c81443e5a 100644 --- a/react/features/base/tracks/functions.js +++ b/react/features/base/tracks/functions.js @@ -35,14 +35,16 @@ export function createLocalTracksF( if (typeof APP !== 'undefined') { // TODO The app's settings should go in the redux store and then the // 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) { - cameraDeviceId = APP.settings.getCameraDeviceId(); + cameraDeviceId = settings.cameraDeviceId; } if (typeof micDeviceId === 'undefined' || micDeviceId === null) { - micDeviceId = APP.settings.getMicDeviceId(); + micDeviceId = settings.micDeviceId; } - - store || (store = APP.store); // eslint-disable-line no-param-reassign } const { diff --git a/react/features/base/util/helpers.js b/react/features/base/util/helpers.js index 8cdbbedfe..8a8e97f39 100644 --- a/react/features/base/util/helpers.js +++ b/react/features/base/util/helpers.js @@ -18,3 +18,27 @@ export function getJitsiMeetGlobalNS() { 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; +} diff --git a/react/features/device-selection/actions.js b/react/features/device-selection/actions.js index cdfa09a59..b84a09fa1 100644 --- a/react/features/device-selection/actions.js +++ b/react/features/device-selection/actions.js @@ -1,19 +1,21 @@ /* 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 { + 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 { DeviceSelectionDialog } from './components'; @@ -42,10 +44,12 @@ function _openDeviceSelectionDialogHere() { return dispatch => JitsiMeetJS.mediaDevices.isDeviceListAvailable() .then(isDeviceListAvailable => { + const settings = APP.store.getState()['features/base/settings']; + dispatch(openDialog(DeviceSelectionDialog, { - currentAudioInputId: APP.settings.getMicDeviceId(), - currentAudioOutputId: APP.settings.getAudioOutputDeviceId(), - currentVideoInputId: APP.settings.getCameraDeviceId(), + currentAudioInputId: settings.micDeviceId, + currentAudioOutputId: getAudioOutputDeviceId(), + currentVideoInputId: settings.cameraDeviceId, disableAudioInputChange: !JitsiMeetJS.isMultipleAudioInputSupported(), disableDeviceChange: !isDeviceListAvailable @@ -135,6 +139,9 @@ function _openDeviceSelectionDialogInPopup() { */ function _processRequest(dispatch, getState, request, responseCallback) { // eslint-disable-line max-len, max-params if (request.type === 'devices') { + const state = getState(); + const settings = state['features/base/settings']; + switch (request.name) { case 'isDeviceListAvailable': JitsiMeetJS.mediaDevices.isDeviceListAvailable() @@ -152,9 +159,9 @@ function _processRequest(dispatch, getState, request, responseCallback) { // esl break; case 'getCurrentDevices': responseCallback({ - audioInput: APP.settings.getMicDeviceId(), - audioOutput: APP.settings.getAudioOutputDeviceId(), - videoInput: APP.settings.getCameraDeviceId() + audioInput: settings.micDeviceId, + audioOutput: getAudioOutputDeviceId(), + videoInput: settings.cameraDeviceId }); break; case 'getAvailableDevices': diff --git a/react/features/settings/components/AbstractSettingsView.js b/react/features/settings/components/AbstractSettingsView.js index da2e7f35b..8cee672f7 100644 --- a/react/features/settings/components/AbstractSettingsView.js +++ b/react/features/settings/components/AbstractSettingsView.js @@ -2,7 +2,7 @@ import { Component } from 'react'; -import { updateProfile } from '../../base/profile'; +import { updateSettings } from '../../base/settings'; /** * The type of the React {@code Component} props of @@ -11,19 +11,17 @@ import { updateProfile } from '../../base/profile'; type Props = { /** - * The current profile object. - * - * @protected - */ - _profile: Object, - - /** - * The default URL for when there is no custom URL set in the profile. + * The default URL for when there is no custom URL set in the settings. * * @protected */ _serverURL: string, + /** + * The current settings object. + */ + _settings: Object, + /** * Whether {@link AbstractSettingsView} is visible. * @@ -79,7 +77,7 @@ export class AbstractSettingsView extends Component { * @returns {void} */ _onChangeDisplayName(text) { - this._updateProfile({ + this._updateSettings({ displayName: text }); } @@ -94,7 +92,7 @@ export class AbstractSettingsView extends Component { * @returns {void} */ _onChangeEmail(text) { - this._updateProfile({ + this._updateSettings({ email: text }); } @@ -109,7 +107,7 @@ export class AbstractSettingsView extends Component { * @returns {void} */ _onChangeServerURL(text) { - this._updateProfile({ + this._updateSettings({ serverURL: text }); } @@ -125,7 +123,7 @@ export class AbstractSettingsView extends Component { * @returns {void} */ _onStartAudioMutedChange(newValue) { - this._updateProfile({ + this._updateSettings({ startWithAudioMuted: newValue }); } @@ -141,22 +139,23 @@ export class AbstractSettingsView extends Component { * @returns {void} */ _onStartVideoMutedChange(newValue) { - this._updateProfile({ + this._updateSettings({ 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 * @returns {void} */ - _updateProfile(updateObject: Object) { - this.props.dispatch(updateProfile(updateObject)); + _updateSettings(updateObject: Object) { + this.props.dispatch(updateSettings(updateObject)); } } @@ -167,15 +166,15 @@ export class AbstractSettingsView extends Component { * @param {Object} state - The redux state. * @protected * @returns {{ - * _profile: Object, * _serverURL: string, + * _settings: Object, * _visible: boolean * }} */ export function _mapStateToProps(state: Object) { return { - _profile: state['features/base/profile'], _serverURL: state['features/app'].app._getDefaultURL(), + _settings: state['features/base/settings'], _visible: state['features/settings'].visible }; } diff --git a/react/features/settings/components/native/SettingsView.js b/react/features/settings/components/native/SettingsView.js index 99d851740..36974e45f 100644 --- a/react/features/settings/components/native/SettingsView.js +++ b/react/features/settings/components/native/SettingsView.js @@ -120,7 +120,7 @@ class SettingsView extends AbstractSettingsView { * @returns {void} */ _processServerURL(hideOnSuccess: boolean) { - const { serverURL } = this.props._profile; + const { serverURL } = this.props._settings; const normalizedURL = normalizeUserInputURL(serverURL); if (normalizedURL === null) { @@ -140,7 +140,7 @@ class SettingsView extends AbstractSettingsView { * @returns {React$Element} */ _renderBody() { - const { _profile } = this.props; + const { _settings } = this.props; return ( @@ -154,7 +154,7 @@ class SettingsView extends AbstractSettingsView { autoCorrect = { false } onChangeText = { this._onChangeDisplayName } placeholder = 'John Doe' - value = { _profile.displayName } /> + value = { _settings.displayName } /> + value = { _settings.email } /> @@ -176,19 +176,19 @@ class SettingsView extends AbstractSettingsView { onBlur = { this._onBlurServerURL } onChangeText = { this._onChangeServerURL } placeholder = { this.props._serverURL } - value = { _profile.serverURL } /> + value = { _settings.serverURL } /> + value = { _settings.startWithAudioMuted } /> + value = { _settings.startWithVideoMuted } /> diff --git a/react/features/welcome/components/AbstractWelcomePage.js b/react/features/welcome/components/AbstractWelcomePage.js index 7d8f2aaf3..bf8e0dfaa 100644 --- a/react/features/welcome/components/AbstractWelcomePage.js +++ b/react/features/welcome/components/AbstractWelcomePage.js @@ -14,10 +14,18 @@ import { generateRoomWithoutSeparator } from '../functions'; type Props = { /** - * The user's profile. + * Room name to join to. */ - _profile: Object, _room: string, + + /** + * The current settings. + */ + _settings: Object, + + /** + * The Redux dispatch Function. + */ dispatch: Dispatch<*> }; @@ -229,13 +237,13 @@ export class AbstractWelcomePage extends Component { * @param {Object} state - The redux state. * @protected * @returns {{ - * _profile: Object, - * _room: string + * _room: string, + * _settings: Object * }} */ export function _mapStateToProps(state: Object) { return { - _profile: state['features/base/profile'], - _room: state['features/base/conference'].room + _room: state['features/base/conference'].room, + _settings: state['features/base/settings'] }; } diff --git a/react/features/welcome/components/VideoSwitch.js b/react/features/welcome/components/VideoSwitch.js index ee9070dab..a895854e6 100644 --- a/react/features/welcome/components/VideoSwitch.js +++ b/react/features/welcome/components/VideoSwitch.js @@ -5,8 +5,8 @@ import { Switch, TouchableWithoutFeedback, View } from 'react-native'; import { connect } from 'react-redux'; import { translate } from '../../base/i18n'; -import { updateProfile } from '../../base/profile'; import { Header, Text } from '../../base/react'; +import { updateSettings } from '../../base/settings'; import styles, { SWITCH_THUMB_COLOR, SWITCH_UNDER_COLOR } from './styles'; @@ -26,9 +26,9 @@ type Props = { 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 { * @inheritdoc */ render() { - const { t, _profile } = this.props; + const { t, _settings } = this.props; const { textStyle } = Header; return ( @@ -71,7 +71,7 @@ class VideoSwitch extends Component { onValueChange = { this._onStartAudioOnlyChange } style = { styles.audioVideoSwitch } thumbTintColor = { SWITCH_THUMB_COLOR } - value = { _profile.startAudioOnly } /> + value = { _settings.startAudioOnly } /> @@ -94,8 +94,7 @@ class VideoSwitch extends Component { _onStartAudioOnlyChange(startAudioOnly) { const { dispatch } = this.props; - dispatch(updateProfile({ - ...this.props._profile, + dispatch(updateSettings({ startAudioOnly })); } @@ -124,12 +123,12 @@ class VideoSwitch extends Component { * @param {Object} state - The redux state. * @protected * @returns {{ - * _profile: Object + * _settings: Object * }} */ export function _mapStateToProps(state: Object) { return { - _profile: state['features/base/profile'] + _settings: state['features/base/settings'] }; } diff --git a/react/features/welcome/components/WelcomePage.native.js b/react/features/welcome/components/WelcomePage.native.js index c89de7513..1ac71b442 100644 --- a/react/features/welcome/components/WelcomePage.native.js +++ b/react/features/welcome/components/WelcomePage.native.js @@ -66,7 +66,7 @@ class WelcomePage extends AbstractWelcomePage { const { dispatch } = this.props; - if (this.props._profile.startAudioOnly) { + if (this.props._settings.startAudioOnly) { dispatch(destroyLocalTracks()); } else { dispatch(createDesiredLocalTracks(MEDIA_TYPE.VIDEO));