diff --git a/react/features/app/actions.js b/react/features/app/actions.js index 537ff7c77..6f0deb3d0 100644 --- a/react/features/app/actions.js +++ b/react/features/app/actions.js @@ -4,7 +4,6 @@ import { setRoom } from '../base/conference'; import { configWillLoad, loadConfigError, setConfig } from '../base/config'; import { setLocationURL } from '../base/connection'; import { loadConfig } from '../base/lib-jitsi-meet'; -import { getProfile } from '../base/profile'; import { parseURIString } from '../base/util'; import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes'; @@ -15,7 +14,7 @@ declare var APP: Object; * Triggers an in-app navigation to a specific route. Allows navigation to be * abstracted between the mobile/React Native and Web/React applications. * - * @param {(string|undefined)} uri - The URI to which to navigate. It may be a + * @param {string|undefined} uri - The URI to which to navigate. It may be a * full URL with an HTTP(S) scheme, a full or partial URI with the app-specific * scheme, or a mere room name. * @returns {Function} @@ -83,11 +82,10 @@ function _appNavigateToMandatoryLocation( }); } - const profile = getProfile(getState()); + const profile = getState()['features/base/profile']; - return promise.then(() => dispatch(setConfig( - _mergeConfigWithProfile(config, profile) - ))); + return promise.then(() => + dispatch(setConfig(_mergeConfigWithProfile(config, profile)))); } } diff --git a/react/features/app/components/AbstractApp.js b/react/features/app/components/AbstractApp.js index f9a1c22b1..124fdf33c 100644 --- a/react/features/app/components/AbstractApp.js +++ b/react/features/app/components/AbstractApp.js @@ -12,7 +12,7 @@ import { localParticipantJoined, localParticipantLeft } from '../../base/participants'; -import { getProfile } from '../../base/profile'; +import '../../base/profile'; import { Fragment, RouteRegistry } from '../../base/react'; import { MiddlewareRegistry, ReducerRegistry } from '../../base/redux'; import { PersistenceRegistry } from '../../base/storage'; @@ -123,7 +123,7 @@ export class AbstractApp extends Component { */ componentWillMount() { this._init.then(() => { - const { dispatch } = this._getStore(); + const { dispatch, getState } = this._getStore(); dispatch(appWillMount(this)); @@ -144,7 +144,7 @@ export class AbstractApp extends Component { } // Profile is the new React compatible settings. - const profile = getProfile(this._getStore().getState()); + const profile = getState()['features/base/profile']; if (profile) { localParticipant.email @@ -381,7 +381,8 @@ export class AbstractApp extends Component { return ( this.props.defaultURL - || getProfile(this._getStore().getState()).serverURL + || this._getStore().getState()['features/base/profile'] + .serverURL || DEFAULT_URL); } diff --git a/react/features/base/conference/middleware.js b/react/features/base/conference/middleware.js index 405db843d..1c749ff22 100644 --- a/react/features/base/conference/middleware.js +++ b/react/features/base/conference/middleware.js @@ -128,7 +128,7 @@ function _conferenceFailedOrLeft({ dispatch, getState }, next, action) { const state = getState(); const { audioOnly } = state['features/base/conference']; - const { startAudioOnly } = state['features/base/profile'].profile; + const { startAudioOnly } = state['features/base/profile']; // FIXME: Consider implementing a standalone audio-only feature that handles // all these state changes. diff --git a/react/features/base/profile/functions.js b/react/features/base/profile/functions.js deleted file mode 100644 index eb43c5348..000000000 --- a/react/features/base/profile/functions.js +++ /dev/null @@ -1,15 +0,0 @@ -/* @flow */ - -/** - * Retreives the current profile settings from redux store. The profile - * is persisted to localStorage so it's a good candidate to store settings - * in it. - * - * @param {Object} state - The Redux state. - * @returns {Object} - */ -export function getProfile(state: Object) { - const profileStateSlice = state['features/base/profile']; - - return profileStateSlice || {}; -} diff --git a/react/features/base/profile/index.js b/react/features/base/profile/index.js index eea8d7393..dbf9c8839 100644 --- a/react/features/base/profile/index.js +++ b/react/features/base/profile/index.js @@ -1,5 +1,4 @@ export * from './actions'; -export * from './functions'; import './middleware'; import './reducer'; diff --git a/react/features/base/profile/middleware.js b/react/features/base/profile/middleware.js index 71ab11dcc..3c87b52b1 100644 --- a/react/features/base/profile/middleware.js +++ b/react/features/base/profile/middleware.js @@ -2,7 +2,6 @@ import { setAudioOnly } from '../conference'; import { getLocalParticipant, participantUpdated } from '../participants'; -import { getProfile } from '../profile'; import { MiddlewareRegistry, toState } from '../redux'; import { PROFILE_UPDATED } from './actionTypes'; @@ -33,11 +32,11 @@ MiddlewareRegistry.register(store => next => action => { * @param {Object} action - The redux action. * @returns {void} */ -function _maybeUpdateStartAudioOnly(store, action) { - const { profile } = action; - - if (typeof profile.startAudioOnly === 'boolean') { - store.dispatch(setAudioOnly(profile.startAudioOnly)); +function _maybeUpdateStartAudioOnly( + { dispatch }, + { profile: { startAudioOnly } }) { + if (typeof startAudioOnly === 'boolean') { + dispatch(setAudioOnly(startAudioOnly)); } } @@ -50,7 +49,7 @@ function _maybeUpdateStartAudioOnly(store, action) { function _updateLocalParticipant(store) { const state = toState(store); const localParticipant = getLocalParticipant(state); - const profile = getProfile(state); + const profile = state['features/base/profile']; store.dispatch(participantUpdated({ // Identify that the participant to update i.e. the local participant: diff --git a/react/features/base/profile/reducer.js b/react/features/base/profile/reducer.js index e65e07faf..0d8553faa 100644 --- a/react/features/base/profile/reducer.js +++ b/react/features/base/profile/reducer.js @@ -1,26 +1,53 @@ // @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 base/profile. + * Sets up the persistence of the feature {@code base/profile}. */ PersistenceRegistry.register(STORE_NAME); -ReducerRegistry.register( - STORE_NAME, (state = {}, action) => { - switch (action.type) { - case PROFILE_UPDATED: - return { - ...state, - ...action.profile - }; - } +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; - return 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/storage/PersistenceRegistry.js b/react/features/base/storage/PersistenceRegistry.js index 29aea68b8..cb48249a8 100644 --- a/react/features/base/storage/PersistenceRegistry.js +++ b/react/features/base/storage/PersistenceRegistry.js @@ -1,25 +1,23 @@ // @flow -import Logger from 'jitsi-meet-logger'; import md5 from 'js-md5'; -const logger = Logger.getLogger(__filename); +const logger = require('jitsi-meet-logger').getLogger(__filename); /** - * The name of the localStorage store where the app persists its values to. + * The name of the {@code localStorage} store where the app persists its values. */ const PERSISTED_STATE_NAME = 'jitsi-state'; /** - * Mixed type of the element (subtree) config. If it's a boolean, - * (and is true) we persist the entire subtree. If it's an object, - * we perist a filtered subtree based on the properties in the - * config object. + * Mixed type of the element (subtree) config. If it's a {@code boolean} (and is + * {@code true}), we persist the entire subtree. If it's an {@code Object}, we + * perist a filtered subtree based on the properties of the config object. */ -declare type ElementConfig = Object | boolean; +declare type ElementConfig = boolean | Object; /** - * The type of the name-config pairs stored in this reducer. + * The type of the name-config pairs stored in {@code PersistenceRegistry}. */ declare type PersistencyConfigMap = { [name: string]: ElementConfig }; @@ -30,74 +28,64 @@ declare type PersistencyConfigMap = { [name: string]: ElementConfig }; class PersistenceRegistry { _checksum: string; - _elements: PersistencyConfigMap; + _elements: PersistencyConfigMap = {}; /** - * Initializes a new {@ code PersistenceRegistry} instance. - */ - constructor() { - this._elements = {}; - } - - /** - * Returns the persisted redux state. This function takes the - * {@link #_elements} into account as we may have persisted something in the - * past that we don't want to retreive anymore. The next - * {@link #persistState} will remove those values. + * Returns the persisted redux state. Takes the {@link #_elements} into + * account as we may have persisted something in the past that we don't want + * to retreive anymore. The next {@link #persistState} will remove such + * values. * * @returns {Object} */ getPersistedState() { let filteredPersistedState = {}; - let persistedState = window.localStorage.getItem(PERSISTED_STATE_NAME); - if (persistedState) { - // This is the legacy implementation, - // must be removed in a later version. - try { - persistedState = JSON.parse(persistedState); - } catch (error) { - logger.error( - 'Error parsing persisted state', - persistedState, - error); - persistedState = {}; - } + // localStorage key per feature + for (const subtreeName of Object.keys(this._elements)) { + // Assumes that the persisted value is stored under the same key as + // the feature's redux state name. + // TODO We'll need to introduce functions later that can control the + // persist key's name. Similar to control serialization and + // deserialization. But that should be a straightforward change. + const persistedSubtree + = this._getPersistedSubtree( + subtreeName, + this._elements[subtreeName]); - filteredPersistedState - = this._getFilteredState(persistedState); - - // legacy values must be written to the new store format and - // old values to be deleted, so then it'll never be used again. - this.persistState(filteredPersistedState); - window.localStorage.removeItem(PERSISTED_STATE_NAME); - } else { - // new, split-keys implementation - for (const subtreeName of Object.keys(this._elements)) { - /* - * this assumes that the persisted value is stored under the - * same key as the feature's redux state name. - * We'll need to introduce functions later that can control - * the persist key's name. Similar to control serialization - * and deserialization. - * But that should be a straightforward change. - */ - const persistedSubtree - = this._getPersistedSubtree( - subtreeName, - this._elements[subtreeName] - ); - - if (persistedSubtree !== undefined) { - filteredPersistedState[subtreeName] = persistedSubtree; - } + if (persistedSubtree !== undefined) { + filteredPersistedState[subtreeName] = persistedSubtree; } } - // initialize checksum + // legacy + if (Object.keys(filteredPersistedState).length === 0) { + const { localStorage } = window; + let persistedState = localStorage.getItem(PERSISTED_STATE_NAME); + + if (persistedState) { + try { + persistedState = JSON.parse(persistedState); + } catch (error) { + logger.error( + 'Error parsing persisted state', + persistedState, + error); + persistedState = {}; + } + + filteredPersistedState = this._getFilteredState(persistedState); + + // Store into the new format and delete the old format so that + // it's not used again. + this.persistState(filteredPersistedState); + localStorage.removeItem(PERSISTED_STATE_NAME); + } + } + + // Initialize the checksum. this._checksum = this._calculateChecksum(filteredPersistedState); - this._checksum = this._calculateChecksum(filteredPersistedState); logger.info('redux state rehydrated as', filteredPersistedState); return filteredPersistedState; @@ -112,26 +100,25 @@ class PersistenceRegistry { */ persistState(state: Object) { const filteredState = this._getFilteredState(state); - const newCheckSum = this._calculateChecksum(filteredState); + const checksum = this._calculateChecksum(filteredState); - if (newCheckSum !== this._checksum) { + if (checksum !== this._checksum) { for (const subtreeName of Object.keys(filteredState)) { try { window.localStorage.setItem( subtreeName, JSON.stringify(filteredState[subtreeName])); } catch (error) { - logger.error('Error persisting redux subtree', + logger.error( + 'Error persisting redux subtree', subtreeName, filteredState[subtreeName], - error - ); + error); } } logger.info( - `redux state persisted. ${this._checksum} -> ${ - newCheckSum}`); - this._checksum = newCheckSum; + `redux state persisted. ${this._checksum} -> ${checksum}`); + this._checksum = checksum; } } @@ -139,8 +126,8 @@ class PersistenceRegistry { * Registers a new subtree config to be used for the persistency. * * @param {string} name - The name of the subtree the config belongs to. - * @param {ElementConfig} config - The config object, or boolean - * if the entire subtree needs to be persisted. + * @param {ElementConfig} config - The config {@code Object}, or + * {@code boolean} if the entire subtree needs to be persisted. * @returns {void} */ register(name: string, config?: ElementConfig = true) { @@ -148,32 +135,81 @@ class PersistenceRegistry { } /** - * Calculates the checksum of the current or the new values of the state. + * Calculates the checksum of a specific state. * + * @param {Object} state - The redux state to calculate the checksum of. * @private - * @param {Object} filteredState - The filtered/persisted redux state. - * @returns {string} + * @returns {string} The checksum of the specified {@code state}. */ - _calculateChecksum(filteredState: Object) { + _calculateChecksum(state: Object) { try { - return md5.hex(JSON.stringify(filteredState) || ''); + return md5.hex(JSON.stringify(state) || ''); } catch (error) { - logger.error( - 'Error calculating checksum for state', - filteredState, - error); + logger.error('Error calculating checksum for state', state, error); return ''; } } + /** + * Prepares a filtered state from the actual or the persisted redux state, + * based on this registry. + * + * @param {Object} state - The actual or persisted redux state. + * @private + * @returns {Object} + */ + _getFilteredState(state: Object) { + const filteredState = {}; + + for (const name of Object.keys(this._elements)) { + if (state[name]) { + filteredState[name] + = this._getFilteredSubtree( + state[name], + this._elements[name]); + } + } + + return filteredState; + } + + /** + * Prepares a filtered subtree based on the config for persisting or for + * retrieval. + * + * @param {Object} subtree - The redux state subtree. + * @param {ElementConfig} subtreeConfig - The related config. + * @private + * @returns {Object} + */ + _getFilteredSubtree(subtree, subtreeConfig) { + let filteredSubtree; + + if (typeof subtreeConfig === 'object') { + // Only a filtered subtree gets persisted as specified by + // subtreeConfig. + filteredSubtree = {}; + for (const persistedKey of Object.keys(subtree)) { + if (subtreeConfig[persistedKey]) { + filteredSubtree[persistedKey] = subtree[persistedKey]; + } + } + } else if (subtreeConfig) { + // Persist the entire subtree. + filteredSubtree = subtree; + } + + return filteredSubtree; + } + /** * Retreives a persisted subtree from the storage. * - * @private * @param {string} subtreeName - The name of the subtree. - * @param {Object} subtreeConfig - The config of the subtree - * from this._elements. + * @param {Object} subtreeConfig - The config of the subtree from + * {@link #_elements}. + * @private * @returns {Object} */ _getPersistedSubtree(subtreeName, subtreeConfig) { @@ -182,6 +218,7 @@ class PersistenceRegistry { if (persistedSubtree) { try { persistedSubtree = JSON.parse(persistedSubtree); + const filteredSubtree = this._getFilteredSubtree(persistedSubtree, subtreeConfig); @@ -197,58 +234,7 @@ class PersistenceRegistry { } } - return null; - } - - /** - * Prepares a filtered state from the actual or the persisted redux state, - * based on this registry. - * - * @private - * @param {Object} state - The actual or persisted redux state. - * @returns {Object} - */ - _getFilteredState(state: Object) { - const filteredState = {}; - - for (const name of Object.keys(this._elements)) { - if (state[name]) { - filteredState[name] = this._getFilteredSubtree( - state[name], - this._elements[name]); - } - } - - return filteredState; - } - - /** - * Prepares a filtered subtree based on the config for persisting or for - * retrieval. - * - * @private - * @param {Object} subtree - The redux state subtree. - * @param {ElementConfig} subtreeConfig - The related config. - * @returns {Object} - */ - _getFilteredSubtree(subtree, subtreeConfig) { - let filteredSubtree; - - if (subtreeConfig === true) { - // we persist the entire subtree - filteredSubtree = subtree; - } else if (typeof subtreeConfig === 'object') { - // only a filtered subtree gets persisted, based on the - // subtreeConfig object. - filteredSubtree = {}; - for (const persistedKey of Object.keys(subtree)) { - if (subtreeConfig[persistedKey]) { - filteredSubtree[persistedKey] = subtree[persistedKey]; - } - } - } - - return filteredSubtree; + return undefined; } } diff --git a/react/features/recent-list/components/AbstractRecentList.js b/react/features/recent-list/components/AbstractRecentList.js index 17e7609d4..e33eeb0a8 100644 --- a/react/features/recent-list/components/AbstractRecentList.js +++ b/react/features/recent-list/components/AbstractRecentList.js @@ -8,16 +8,19 @@ import { appNavigate } from '../../app'; * The type of the React {@code Component} props of {@link AbstractRecentList} */ type Props = { + _defaultURL: string, - /** - * Indicates if the list is disabled or not. - */ - disabled: boolean, + _recentList: Array, /** * The redux store's {@code dispatch} function. */ - dispatch: Dispatch<*> + dispatch: Dispatch<*>, + + /** + * Whether {@code AbstractRecentList} is enabled. + */ + enabled: boolean }; /** @@ -33,18 +36,20 @@ export default class AbstractRecentList extends Component { * Joins the selected room. * * @param {string} room - The selected room. + * @protected * @returns {void} */ _onJoin(room) { - const { disabled, dispatch } = this.props; + const { dispatch, enabled } = this.props; - !disabled && room && dispatch(appNavigate(room)); + enabled && room && dispatch(appNavigate(room)); } /** * Creates a bound onPress action for the list item. * * @param {string} room - The selected room. + * @protected * @returns {Function} */ _onSelect(room) { @@ -53,17 +58,18 @@ export default class AbstractRecentList extends Component { } /** - * Maps redux state to component props. + * Maps (parts of) the redux state into {@code AbstractRecentList}'s React + * {@code Component} props. * * @param {Object} state - The redux state. * @returns {{ - * _homeServer: string, - * _recentList: Array + * _defaultURL: string, + * _recentList: Array * }} */ export function _mapStateToProps(state: Object) { return { - _homeServer: state['features/app'].app._getDefaultURL(), + _defaultURL: state['features/app'].app._getDefaultURL(), _recentList: state['features/recent-list'] }; } diff --git a/react/features/recent-list/components/RecentList.native.js b/react/features/recent-list/components/RecentList.native.js index 627b9a83c..26cce2590 100644 --- a/react/features/recent-list/components/RecentList.native.js +++ b/react/features/recent-list/components/RecentList.native.js @@ -47,21 +47,20 @@ class RecentList extends AbstractRecentList { * @returns {ReactElement} */ render() { - const { _recentList, disabled } = this.props; + const { enabled, _recentList } = this.props; if (!_recentList) { return null; } const listViewDataSource - = this.dataSource.cloneWithRows( - getRecentRooms(_recentList)); + = this.dataSource.cloneWithRows(getRecentRooms(_recentList)); return ( } */ - _getAvatarStyle(recentListEntry) { + _getAvatarStyle({ baseURL, serverName }) { const avatarStyles = [ styles.avatar ]; - if (recentListEntry.baseURL !== this.props._homeServer) { - avatarStyles.push( - this._getColorForServerName(recentListEntry.serverName)); + if (baseURL !== this.props._defaultURL) { + avatarStyles.push(this._getColorForServerName(serverName)); } return avatarStyles; @@ -115,40 +114,15 @@ class RecentList extends AbstractRecentList { * @private * @returns {ReactElement} */ - _renderConfDuration({ conferenceDurationString }) { - if (conferenceDurationString) { + _renderConfDuration({ durationString }) { + if (durationString) { return ( - { conferenceDurationString } - - - ); - } - - return null; - } - - /** - * Renders the server info component based on if the entry was on a - * different server or not. - * - * @param {Object} recentListEntry - The recent list entry being rendered. - * @private - * @returns {ReactElement} - */ - _renderServerInfo(recentListEntry) { - if (recentListEntry.baseURL !== this.props._homeServer) { - return ( - - - - { recentListEntry.serverName } + { durationString } ); @@ -191,17 +165,38 @@ class RecentList extends AbstractRecentList { { data.dateString } - { - this._renderConfDuration(data) - } - { - this._renderServerInfo(data) - } + { this._renderConfDuration(data) } + { this._renderServerInfo(data) } ); } + + /** + * Renders the server info component based on whether the entry was on a + * different server. + * + * @param {Object} recentListEntry - The recent list entry being rendered. + * @private + * @returns {ReactElement} + */ + _renderServerInfo({ baseURL, serverName }) { + if (baseURL !== this.props._defaultURL) { + return ( + + + + { serverName } + + + ); + } + + return null; + } } export default connect(_mapStateToProps)(RecentList); diff --git a/react/features/recent-list/functions.js b/react/features/recent-list/functions.js index 5c7b2aeb6..bc0adb1be 100644 --- a/react/features/recent-list/functions.js +++ b/react/features/recent-list/functions.js @@ -17,7 +17,6 @@ require('moment/locale/it'); require('moment/locale/nb'); // OC is not available. Please submit OC translation to the MomentJS project. - require('moment/locale/pl'); require('moment/locale/pt'); require('moment/locale/pt-br'); @@ -47,22 +46,22 @@ export function getRecentRooms(list: Array): Array { const locale = _getSupportedLocale(); for (const e of list) { - const location = parseURIString(e.conference); + const uri = parseURIString(e.conference); + + if (uri && uri.room && uri.hostname) { + const duration + = e.duration || /* legacy */ e.conferenceDuration; - if (location && location.room && location.hostname) { recentRoomDS.push({ - baseURL: `${location.protocol}//${location.host}`, + baseURL: `${uri.protocol}//${uri.host}`, conference: e.conference, - conferenceDuration: e.conferenceDuration, - conferenceDurationString: - _getDurationString( - e.conferenceDuration, - locale), dateString: _getDateString(e.date, locale), dateTimeStamp: e.date, - initials: _getInitials(location.room), - room: location.room, - serverName: location.hostname + duration, + durationString: _getDurationString(duration, locale), + initials: _getInitials(uri.room), + room: uri.room, + serverName: uri.hostname }); } } @@ -124,7 +123,7 @@ function _getInitials(room: string) { * or duration ({@code number}). * * @private - * @param {Date | number} dateOrDuration - The date or duration to format. + * @param {Date|number} dateOrDuration - The date or duration to format. * @param {string} locale - The locale to init the formatter with. Note: The * specified locale must be supported by the formatter so ensure the * prerequisite is met before invoking the function. diff --git a/react/features/recent-list/middleware.js b/react/features/recent-list/middleware.js index 9f9815620..121a8f38f 100644 --- a/react/features/recent-list/middleware.js +++ b/react/features/recent-list/middleware.js @@ -15,11 +15,11 @@ import { storeCurrentConference, updateConferenceDuration } from './actions'; MiddlewareRegistry.register(store => next => action => { switch (action.type) { case CONFERENCE_WILL_LEAVE: - _updateConferenceDuration(store, next); + _updateConferenceDuration(store); break; case SET_ROOM: - _maybeStoreCurrentConference(store, next, action); + _maybeStoreCurrentConference(store, action); break; } @@ -36,12 +36,11 @@ MiddlewareRegistry.register(store => next => action => { * @private * @returns {void} */ -function _maybeStoreCurrentConference(store, next, action) { - const { locationURL } = store.getState()['features/base/connection']; - const { room } = action; - +function _maybeStoreCurrentConference({ dispatch, getState }, { room }) { if (room) { - next(storeCurrentConference(locationURL)); + const { locationURL } = getState()['features/base/connection']; + + dispatch(storeCurrentConference(locationURL)); } } @@ -49,13 +48,11 @@ function _maybeStoreCurrentConference(store, next, action) { * Updates the duration of the last conference stored in the list. * * @param {Store} store - The redux store. - * @param {Dispatch} next - The redux {@code dispatch} function. - * @param {Action} action - The redux action. * @private * @returns {void} */ -function _updateConferenceDuration(store, next) { - const { locationURL } = store.getState()['features/base/connection']; +function _updateConferenceDuration({ dispatch, getState }) { + const { locationURL } = getState()['features/base/connection']; - next(updateConferenceDuration(locationURL)); + dispatch(updateConferenceDuration(locationURL)); } diff --git a/react/features/recent-list/reducer.js b/react/features/recent-list/reducer.js index 4dea016fb..b9f0f5a82 100644 --- a/react/features/recent-list/reducer.js +++ b/react/features/recent-list/reducer.js @@ -1,5 +1,6 @@ // @flow +import { APP_WILL_MOUNT } from '../app'; import { ReducerRegistry } from '../base/redux'; import { PersistenceRegistry } from '../base/storage'; @@ -10,6 +11,13 @@ import { const logger = require('jitsi-meet-logger').getLogger(__filename); +/** + * The default/initial redux state of the feature {@code recent-list}. + * + * @type {Array} + */ +const DEFAULT_STATE = []; + /** * The name of the {@code window.localStorage} item where recent rooms are * stored. @@ -31,17 +39,20 @@ export const MAX_LIST_SIZE = 30; const STORE_NAME = 'features/recent-list'; /** - * Sets up the persistence of the feature recent-list. + * Sets up the persistence of the feature {@code recent-list}. */ PersistenceRegistry.register(STORE_NAME); /** - * Reduces the redux actions of the feature recent-list. + * Reduces redux actions for the purposes of the feature {@code recent-list}. */ ReducerRegistry.register( STORE_NAME, (state = _getLegacyRecentRoomList(), action) => { switch (action.type) { + case APP_WILL_MOUNT: + return _appWillMount(state); + case STORE_CURRENT_CONFERENCE: return _storeCurrentConference(state, action); @@ -53,18 +64,48 @@ ReducerRegistry.register( } }); +/** + * Reduces the redux action {@link APP_WILL_MOUNT}. + * + * @param {Object} state - The redux state of the feature {@code recent-list}. + * @param {Action} action - The redux action {@code APP_WILL_MOUNT}. + * @returns {Array} The next redux state of the feature + * {@code recent-list}. + */ +function _appWillMount(state) { + // 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 recent-list's state is in the format deemed current by the + // current app revision. + if (state && typeof state === 'object') { + if (Array.isArray(state)) { + return state; + } + + // In an enterprise/internal build of Jitsi Meet for Android and iOS we + // had recent-list's state as an object with property list. + const { list } = state; + + if (Array.isArray(list) && list.length) { + return list.slice(); + } + } + + // In the weird case that we have previously persisted/serialized null. + return DEFAULT_STATE; +} + /** * Retrieves the recent room list that was stored using the legacy way. * * @returns {Array} */ -export function _getLegacyRecentRoomList(): Array { +function _getLegacyRecentRoomList(): Array { try { - const list - = JSON.parse(window.localStorage.getItem(LEGACY_STORAGE_KEY)); + const str = window.localStorage.getItem(LEGACY_STORAGE_KEY); - if (list && list.length) { - return list; + if (str) { + return JSON.parse(str); } } catch (error) { logger.warn('Failed to parse legacy recent-room list!'); @@ -74,61 +115,79 @@ export function _getLegacyRecentRoomList(): Array { } /** -* Adds a new list entry to the redux store. -* -* @param {Object} state - The redux state. -* @param {Object} action - The redux action. -* @returns {Object} -*/ -function _storeCurrentConference(state, action) { - const { locationURL } = action; + * Adds a new list entry to the redux store. + * + * @param {Object} state - The redux state of the feature {@code recent-list}. + * @param {Object} action - The redux action. + * @returns {Object} + */ +function _storeCurrentConference(state, { locationURL }) { const conference = locationURL.href; // If the current conference is already in the list, we remove it to re-add // it to the top. - const list = (Array.isArray(state) ? state : []) - .filter(e => e.conference !== conference); + const nextState + = state.filter(e => !_urlStringEquals(e.conference, conference)); // The list is a reverse-sorted (i.e. the newer elements are at the end). - list.push({ + nextState.push({ conference, - conferenceDuration: 0, // We don't have this data yet! - date: Date.now() + date: Date.now(), + duration: 0 // We don't have the duration yet! }); // Ensure the list doesn't exceed a/the maximum size. - list.splice(0, list.length - MAX_LIST_SIZE); + nextState.splice(0, nextState.length - MAX_LIST_SIZE); - return list; + return nextState; } /** * Updates the conference length when left. * - * @param {Object} state - The redux state. + * @param {Object} state - The redux state of the feature {@code recent-list}. * @param {Object} action - The redux action. - * @returns {Object} + * @returns {Object} The next redux state of the feature {@code recent-list}. */ -function _updateConferenceDuration(state, action) { - const { locationURL } = action; +function _updateConferenceDuration(state, { locationURL }) { + if (locationURL && locationURL.href && state.length) { + const mostRecentIndex = state.length - 1; + const mostRecent = state[mostRecentIndex]; - if (locationURL && locationURL.href) { - // shallow copy to avoid in-place modification. - const list = (Array.isArray(state) ? state : []).slice(); + if (_urlStringEquals(mostRecent.conference, locationURL.href)) { + // The last conference start was stored so we need to update the + // length. + const nextMostRecent = { + ...mostRecent, + duration: Date.now() - mostRecent.date + }; - if (list.length > 0) { - const mostRecentURL = list[list.length - 1]; + delete nextMostRecent.conferenceDuration; // legacy - if (mostRecentURL.conference === locationURL.href) { - // The last conference start was stored so we need to update the - // length. - mostRecentURL.conferenceDuration - = Date.now() - mostRecentURL.date; + // Shallow copy to avoid in-place modification. + const nextState = state.slice(); - return list; - } + nextState[mostRecentIndex] = nextMostRecent; + + return nextState; } } return state; } + +/** + * Determines whether two specific URL {@code strings} are equal in the sense + * that they identify one and the same conference resource (irrespective of + * time) for the purposes of the feature {@code recent-list}. + * + * @param {string} a - The URL {@code string} to test for equality to {@code b}. + * @param {string} b - The URL {@code string} to test for equality to {@code a}. + * @returns {boolean} + */ +function _urlStringEquals(a: string, b: string) { + // FIXME Case-sensitive comparison is wrong because the room name at least + // is case insensitive on the server and elsewhere (where it matters) in the + // client. I don't think domain names are case-sensitive either. + return a === b; +} diff --git a/react/features/settings/components/AbstractSettingsView.js b/react/features/settings/components/AbstractSettingsView.js index 4a9d9dd44..da2e7f35b 100644 --- a/react/features/settings/components/AbstractSettingsView.js +++ b/react/features/settings/components/AbstractSettingsView.js @@ -2,7 +2,7 @@ import { Component } from 'react'; -import { getProfile, updateProfile } from '../../base/profile'; +import { updateProfile } from '../../base/profile'; /** * The type of the React {@code Component} props of @@ -12,16 +12,22 @@ type Props = { /** * The current profile object. + * + * @protected */ _profile: Object, /** * The default URL for when there is no custom URL set in the profile. + * + * @protected */ _serverURL: string, /** * Whether {@link AbstractSettingsView} is visible. + * + * @protected */ _visible: boolean, @@ -168,7 +174,7 @@ export class AbstractSettingsView extends Component { */ export function _mapStateToProps(state: Object) { return { - _profile: getProfile(state), + _profile: state['features/base/profile'], _serverURL: state['features/app'].app._getDefaultURL(), _visible: state['features/settings'].visible }; diff --git a/react/features/welcome/components/AbstractWelcomePage.js b/react/features/welcome/components/AbstractWelcomePage.js index 3989ed713..7d8f2aaf3 100644 --- a/react/features/welcome/components/AbstractWelcomePage.js +++ b/react/features/welcome/components/AbstractWelcomePage.js @@ -1,6 +1,5 @@ // @flow -import PropTypes from 'prop-types'; import { Component } from 'react'; import { createWelcomePageEvent, sendAnalytics } from '../../analytics'; @@ -14,11 +13,6 @@ import { generateRoomWithoutSeparator } from '../functions'; */ type Props = { - /** - * Boolean to indicate if the room field is focused or not. - */ - _fieldFocused: boolean, - /** * The user's profile. */ @@ -32,17 +26,7 @@ type Props = { * * @abstract */ -export class AbstractWelcomePage extends Component<*, *> { - /** - * {@code AbstractWelcomePage}'s React {@code Component} prop types. - * - * @static - */ - static propTypes = { - _room: PropTypes.string, - dispatch: PropTypes.func - }; - +export class AbstractWelcomePage extends Component { _mounted: ?boolean; /** @@ -245,12 +229,13 @@ export class AbstractWelcomePage extends Component<*, *> { * @param {Object} state - The redux state. * @protected * @returns {{ + * _profile: Object, * _room: string * }} */ export function _mapStateToProps(state: Object) { return { - _profile: state['features/base/profile'].profile, + _profile: state['features/base/profile'], _room: state['features/base/conference'].room }; } diff --git a/react/features/welcome/components/WelcomePage.native.js b/react/features/welcome/components/WelcomePage.native.js index ab27add2c..fc1fa8288 100644 --- a/react/features/welcome/components/WelcomePage.native.js +++ b/react/features/welcome/components/WelcomePage.native.js @@ -40,13 +40,6 @@ import WelcomePageSideBar from './WelcomePageSideBar'; * @extends AbstractWelcomePage */ class WelcomePage extends AbstractWelcomePage { - /** - * WelcomePage component's property types. - * - * @static - */ - static propTypes = AbstractWelcomePage.propTypes; - /** * Constructor of the Component. * @@ -140,7 +133,7 @@ class WelcomePage extends AbstractWelcomePage { { this._renderHintBox() } - +