From 158cadf4f906adf8b98ca94f036941a90b992558 Mon Sep 17 00:00:00 2001 From: zbettenbuk Date: Wed, 17 Jan 2018 12:19:10 +0100 Subject: [PATCH] Improve persistency layer --- react/features/app/components/AbstractApp.js | 6 +- react/features/base/profile/reducer.js | 6 +- .../base/redux/PersistencyRegistry.js | 167 ++++++++++++++++++ react/features/base/redux/functions.js | 93 ---------- react/features/base/redux/index.js | 1 + react/features/base/redux/middleware.js | 4 +- .../features/base/redux/persisterconfig.json | 5 - react/features/base/redux/readme.md | 32 ++-- react/features/recent-list/actionTypes.js | 11 ++ react/features/recent-list/actions.js | 38 ++++ .../components/AbstractRecentList.js | 70 ++------ .../components/RecentList.native.js | 49 +++-- react/features/recent-list/functions.js | 115 +++++------- react/features/recent-list/index.js | 1 + react/features/recent-list/middleware.js | 91 +++------- react/features/recent-list/reducer.js | 106 +++++++++++ 16 files changed, 449 insertions(+), 346 deletions(-) create mode 100644 react/features/base/redux/PersistencyRegistry.js delete mode 100644 react/features/base/redux/persisterconfig.json create mode 100644 react/features/recent-list/actionTypes.js create mode 100644 react/features/recent-list/actions.js create mode 100644 react/features/recent-list/reducer.js diff --git a/react/features/app/components/AbstractApp.js b/react/features/app/components/AbstractApp.js index 1ff8e1c79..1ef67aa42 100644 --- a/react/features/app/components/AbstractApp.js +++ b/react/features/app/components/AbstractApp.js @@ -14,8 +14,8 @@ import { } from '../../base/participants'; import { Fragment, RouteRegistry } from '../../base/react'; import { - getPersistedState, MiddlewareRegistry, + PersistencyRegistry, ReducerRegistry } from '../../base/redux'; import { getProfile } from '../../base/profile'; @@ -346,7 +346,9 @@ export class AbstractApp extends Component { middleware = compose(middleware, devToolsExtension()); } - return createStore(reducer, getPersistedState(), middleware); + return createStore( + reducer, PersistencyRegistry.getPersistedState(), middleware + ); } /** diff --git a/react/features/base/profile/reducer.js b/react/features/base/profile/reducer.js index 2337146c1..e406e177b 100644 --- a/react/features/base/profile/reducer.js +++ b/react/features/base/profile/reducer.js @@ -4,7 +4,7 @@ import { PROFILE_UPDATED } from './actionTypes'; -import { ReducerRegistry } from '../redux'; +import { PersistencyRegistry, ReducerRegistry } from '../redux'; const DEFAULT_STATE = { profile: {} @@ -12,6 +12,10 @@ const DEFAULT_STATE = { const STORE_NAME = 'features/base/profile'; +PersistencyRegistry.register(STORE_NAME, { + profile: true +}); + ReducerRegistry.register( STORE_NAME, (state = DEFAULT_STATE, action) => { switch (action.type) { diff --git a/react/features/base/redux/PersistencyRegistry.js b/react/features/base/redux/PersistencyRegistry.js new file mode 100644 index 000000000..766212cf1 --- /dev/null +++ b/react/features/base/redux/PersistencyRegistry.js @@ -0,0 +1,167 @@ +// @flow +import Logger from 'jitsi-meet-logger'; +import md5 from 'js-md5'; + +const logger = Logger.getLogger(__filename); + +/** + * The name of the localStorage store where the app persists its values to. + */ +const PERSISTED_STATE_NAME = 'jitsi-state'; + +/** + * The type of the name-config pairs stored in this reducer. + */ +declare type PersistencyConfigMap = { [name: string]: Object }; + +/** + * A registry to allow features to register their redux store + * subtree to be persisted and also handles the persistency calls too. + */ +class PersistencyRegistry { + _checksum: string; + _elements: PersistencyConfigMap; + + /** + * Initiates the PersistencyRegistry. + */ + constructor() { + this._elements = {}; + } + + /** + * Returns the persisted redux state. This function takes + * the PersistencyRegistry._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 {Object} + */ + getPersistedState() { + let filteredPersistedState = {}; + let persistedState = window.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); + } + + this._checksum = this._calculateChecksum(filteredPersistedState); + logger.info('Redux state rehydrated as', filteredPersistedState); + + return filteredPersistedState; + } + + /** + * Initiates a persist operation, but its execution will depend on + * the current checksums (checks changes). + * + * @param {Object} state - The redux state. + * @returns {void} + */ + persistState(state: Object) { + const filteredState = this._getFilteredState(state); + const newCheckSum = this._calculateChecksum(filteredState); + + if (newCheckSum !== this._checksum) { + try { + window.localStorage.setItem( + PERSISTED_STATE_NAME, + JSON.stringify(filteredState) + ); + logger.info( + `Redux state persisted. ${this._checksum} -> ${newCheckSum}` + ); + this._checksum = newCheckSum; + } catch (error) { + logger.error('Error persisting Redux state', error); + } + } + } + + /** + * Registers a new subtree config to be used for the persistency. + * + * @param {string} name - The name of the subtree the config belongs to. + * @param {Object} config - The config object. + * @returns {void} + */ + register(name: string, config: Object) { + this._elements[name] = config; + } + + /** + * Calculates the checksum of the current or the new values of the state. + * + * @private + * @param {Object} filteredState - The filtered/persisted Redux state. + * @returns {string} + */ + _calculateChecksum(filteredState: Object) { + try { + return md5.hex(JSON.stringify(filteredState) || ''); + } catch (error) { + logger.error( + 'Error calculating checksum for state', filteredState, error + ); + + return ''; + } + } + + /** + * 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 retreival. + * + * @private + * @param {Object} subtree - The redux state subtree. + * @param {Object} subtreeConfig - The related config. + * @returns {Object} + */ + _getFilteredSubtree(subtree, subtreeConfig) { + const filteredSubtree = {}; + + for (const persistedKey of Object.keys(subtree)) { + if (subtreeConfig[persistedKey]) { + filteredSubtree[persistedKey] + = subtree[persistedKey]; + } + } + + return filteredSubtree; + } +} + +export default new PersistencyRegistry(); diff --git a/react/features/base/redux/functions.js b/react/features/base/redux/functions.js index b2d736a4a..687eb06f4 100644 --- a/react/features/base/redux/functions.js +++ b/react/features/base/redux/functions.js @@ -1,12 +1,6 @@ /* @flow */ import _ from 'lodash'; -import Logger from 'jitsi-meet-logger'; - -import persisterConfig from './persisterconfig.json'; - -const logger = Logger.getLogger(__filename); -const PERSISTED_STATE_NAME = 'jitsi-state'; /** * Sets specific properties of a specific state to specific values and prevents @@ -44,93 +38,6 @@ export function equals(a: any, b: any) { return _.isEqual(a, b); } -/** - * Prepares a filtered state-slice (Redux term) based on the config for - * persisting or for retreival. - * - * @private - * @param {Object} persistedSlice - The redux state-slice. - * @param {Object} persistedSliceConfig - The related config sub-tree. - * @returns {Object} - */ -function _getFilteredSlice(persistedSlice, persistedSliceConfig) { - const filteredpersistedSlice = {}; - - for (const persistedKey of Object.keys(persistedSlice)) { - if (persistedSliceConfig[persistedKey]) { - filteredpersistedSlice[persistedKey] = persistedSlice[persistedKey]; - } - } - - return filteredpersistedSlice; -} - -/** - * Prepares a filtered state from the actual or the - * persisted Redux state, based on the config. - * - * @private - * @param {Object} state - The actual or persisted redux state. - * @returns {Object} - */ -function _getFilteredState(state: Object) { - const filteredState = {}; - - for (const slice of Object.keys(persisterConfig)) { - filteredState[slice] = _getFilteredSlice( - state[slice], - persisterConfig[slice] - ); - } - - return filteredState; -} - -/** - * Returns the persisted redux state. This function takes - * the persisterConfig 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 {Object} - */ -export function getPersistedState() { - let persistedState = window.localStorage.getItem(PERSISTED_STATE_NAME); - - if (persistedState) { - try { - persistedState = JSON.parse(persistedState); - } catch (error) { - return {}; - } - - const filteredPersistedState = _getFilteredState(persistedState); - - logger.info('Redux state rehydrated', filteredPersistedState); - - return filteredPersistedState; - } - - return {}; -} - -/** - * Persists a filtered subtree of the redux state into {@code localStorage}. - * - * @param {Object} state - The redux state. - * @returns {void} - */ -export function persistState(state: Object) { - const filteredState = _getFilteredState(state); - - window.localStorage.setItem( - PERSISTED_STATE_NAME, - JSON.stringify(filteredState) - ); - - logger.info('Redux state persisted'); -} - /** * Sets a specific property of a specific state to a specific value. Prevents * unnecessary state changes (when the specified {@code value} is equal to the diff --git a/react/features/base/redux/index.js b/react/features/base/redux/index.js index aa4e6ec3e..f28de691b 100644 --- a/react/features/base/redux/index.js +++ b/react/features/base/redux/index.js @@ -1,5 +1,6 @@ export * from './functions'; export { default as MiddlewareRegistry } from './MiddlewareRegistry'; +export { default as PersistencyRegistry } from './PersistencyRegistry'; export { default as ReducerRegistry } from './ReducerRegistry'; import './middleware'; diff --git a/react/features/base/redux/middleware.js b/react/features/base/redux/middleware.js index fb2645669..682868a32 100644 --- a/react/features/base/redux/middleware.js +++ b/react/features/base/redux/middleware.js @@ -1,8 +1,8 @@ /* @flow */ import _ from 'lodash'; -import { persistState } from './functions'; import MiddlewareRegistry from './MiddlewareRegistry'; +import PersistencyRegistry from './PersistencyRegistry'; import { toState } from '../redux'; @@ -16,7 +16,7 @@ const PERSIST_DELAY = 2000; * A throttled function to avoid repetitive state persisting. */ const throttledFunc = _.throttle(state => { - persistState(state); + PersistencyRegistry.persistState(state); }, PERSIST_DELAY); /** diff --git a/react/features/base/redux/persisterconfig.json b/react/features/base/redux/persisterconfig.json deleted file mode 100644 index f3cc178f1..000000000 --- a/react/features/base/redux/persisterconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "features/base/profile": { - "profile": true - } -} diff --git a/react/features/base/redux/readme.md b/react/features/base/redux/readme.md index 6abd64f70..a7b7a1b63 100644 --- a/react/features/base/redux/readme.md +++ b/react/features/base/redux/readme.md @@ -1,30 +1,24 @@ Jitsi Meet - redux state persistency ==================================== -Jitsi Meet has a persistency layer that persist a subtree (or specific subtrees) into window.localStorage (on web) or +Jitsi Meet has a persistency layer that persists a subtree (or specific subtrees) into window.localStorage (on web) or AsyncStorage (on mobile). Usage ===== -If a subtree of the redux store should be persisted (e.g. ``'features/base/participants'``), then persistency for that -subtree should be enabled in the config file by creating a key in +If a subtree of the redux store should be persisted (e.g. ``'features/base/profile'``), then persistency for that +subtree should be requested by registering the subtree (and related config) into PersistencyRegistry. +E.g. to register the field ``profile`` of the Redux subtree ``'features/base/profile'`` to be persisted, use: + +```JavaScript +PersistencyRegistry.register('features/base/profile', { + profile: true +}); ``` -react/features/base/redux/persisterconfig.json -``` -and defining all the fields of the subtree that has to be persisted, e.g.: -```json -{ - "features/base/participants": { - "avatarID": true, - "avatarURL": true, - "name": true - }, - "another/subtree": { - "someField": true - } -} -``` -When it's done, Jitsi Meet will persist these subtrees/fields and rehidrate them on startup. + +in the ``reducer.js`` of the ``profile`` feature. + +When it's done, Jitsi Meet will automatically persist these subtrees/fields and rehidrate them on startup. Throttling ========== diff --git a/react/features/recent-list/actionTypes.js b/react/features/recent-list/actionTypes.js new file mode 100644 index 000000000..5df42cb9a --- /dev/null +++ b/react/features/recent-list/actionTypes.js @@ -0,0 +1,11 @@ +// @flow + +/** + * Action type to signal a new addition to the list. + */ +export const STORE_CURRENT_CONFERENCE = Symbol('STORE_CURRENT_CONFERENCE'); + +/** + * Action type to signal that a new conference duration info is available. + */ +export const UPDATE_CONFERENCE_DURATION = Symbol('UPDATE_CONFERENCE_DURATION'); diff --git a/react/features/recent-list/actions.js b/react/features/recent-list/actions.js new file mode 100644 index 000000000..6c7dfe214 --- /dev/null +++ b/react/features/recent-list/actions.js @@ -0,0 +1,38 @@ +// @flow + +import { + STORE_CURRENT_CONFERENCE, + UPDATE_CONFERENCE_DURATION +} from './actionTypes'; + +/** + * Action to initiate a new addition to the list. + * + * @param {Object} locationURL - The current location URL. + * @returns {{ + * type: STORE_CURRENT_CONFERENCE, + * locationURL: Object + * }} + */ +export function storeCurrentConference(locationURL: Object) { + return { + type: STORE_CURRENT_CONFERENCE, + locationURL + }; +} + +/** + * Action to initiate the update of the duration of the last conference. + * + * @param {Object} locationURL - The current location URL. + * @returns {{ + * type: UPDATE_CONFERENCE_DURATION, + * locationURL: Object + * }} + */ +export function updateConferenceDuration(locationURL: Object) { + return { + type: UPDATE_CONFERENCE_DURATION, + locationURL + }; +} diff --git a/react/features/recent-list/components/AbstractRecentList.js b/react/features/recent-list/components/AbstractRecentList.js index 0d83c9183..968b6316a 100644 --- a/react/features/recent-list/components/AbstractRecentList.js +++ b/react/features/recent-list/components/AbstractRecentList.js @@ -1,12 +1,9 @@ // @flow import { Component } from 'react'; -import { ListView } from 'react-native'; import { appNavigate } from '../../app'; -import { getRecentRooms } from '../functions'; - /** * The type of the React {@code Component} props of {@link AbstractRecentList} */ @@ -18,19 +15,6 @@ type Props = { dispatch: Dispatch<*> }; -/** - * The type of the React {@code Component} state of {@link AbstractRecentList}. - */ -type State = { - - /** - * The {@code ListView.DataSource} to be used for the {@code ListView}. Its - * content comes from the native implementation of - * {@code window.localStorage}. - */ - dataSource: Object -}; - /** * Implements a React {@link Component} which represents the list of conferences * recently joined, similar to how a list of last dialed numbers list would do @@ -38,43 +22,7 @@ type State = { * * @extends Component */ -export default class AbstractRecentList extends Component { - - /** - * The datasource that backs the {@code ListView}. - */ - listDataSource = new ListView.DataSource({ - rowHasChanged: (r1, r2) => - r1.conference !== r2.conference - && r1.dateTimeStamp !== r2.dateTimeStamp - }); - - /** - * Initializes a new {@code AbstractRecentList} instance. - */ - constructor() { - super(); - - this.state = { - dataSource: this.listDataSource.cloneWithRows([]) - }; - } - - /** - * Implements React's {@link Component#componentWillMount()}. Invoked - * immediately before mounting occurs. - * - * @inheritdoc - */ - componentWillMount() { - // The following must be done asynchronously because we don't have the - // storage initiated on app startup immediately. - getRecentRooms() - .then(rooms => - this.setState({ - dataSource: this.listDataSource.cloneWithRows(rooms) - })); - } +export default class AbstractRecentList extends Component { /** * Joins the selected room. @@ -96,3 +44,19 @@ export default class AbstractRecentList extends Component { return this._onJoin.bind(this, room); } } + +/** + * Maps Redux state to component props. + * + * @param {Object} state - The redux state. + * @returns {{ + * _homeServer: string, + * _recentList: Array + * }} + */ +export function _mapStateToProps(state: Object) { + return { + _homeServer: state['features/app'].app._getDefaultURL(), + _recentList: state['features/recent-list'].list + }; +} diff --git a/react/features/recent-list/components/RecentList.native.js b/react/features/recent-list/components/RecentList.native.js index 58526e0ad..71effacb1 100644 --- a/react/features/recent-list/components/RecentList.native.js +++ b/react/features/recent-list/components/RecentList.native.js @@ -2,19 +2,32 @@ import React from 'react'; import { ListView, Text, TouchableHighlight, View } from 'react-native'; import { connect } from 'react-redux'; -import { Icon } from '../../base/font-icons'; - -import AbstractRecentList from './AbstractRecentList'; +import AbstractRecentList, { _mapStateToProps } from './AbstractRecentList'; import styles, { UNDERLAY_COLOR } from './styles'; +import { getRecentRooms } from '../functions'; + +import { Icon } from '../../base/font-icons'; + /** * The native container rendering the list of the recently joined rooms. * * @extends AbstractRecentList */ class RecentList extends AbstractRecentList { + /** + * The datasource wrapper to be used for the display. + */ + dataSource = new ListView.DataSource({ + rowHasChanged: (r1, r2) => + r1.conference !== r2.conference + && r1.dateTimeStamp !== r2.dateTimeStamp + }); + /** * Initializes a new {@code RecentList} instance. + * + * @inheritdoc */ constructor() { super(); @@ -35,14 +48,18 @@ class RecentList extends AbstractRecentList { * @returns {ReactElement} */ render() { - if (!this.state.dataSource.getRowCount()) { + if (!this.props || !this.props._recentList) { return null; } + const listViewDataSource = this.dataSource.cloneWithRows( + getRecentRooms(this.props._recentList) + ); + return ( @@ -182,26 +199,4 @@ class RecentList extends AbstractRecentList { } } -/** - * Maps (parts of) the Redux state to the associated RecentList's props. - * - * @param {Object} state - The Redux state. - * @private - * @returns {{ - * _homeServer: string - * }} - */ -function _mapStateToProps(state) { - return { - /** - * The default server name based on which we determine the render - * method. - * - * @private - * @type {string} - */ - _homeServer: state['features/app'].app._getDefaultURL() - }; -} - export default connect(_mapStateToProps)(RecentList); diff --git a/react/features/recent-list/functions.js b/react/features/recent-list/functions.js index eb4447b64..e59fb1a49 100644 --- a/react/features/recent-list/functions.js +++ b/react/features/recent-list/functions.js @@ -5,8 +5,6 @@ import moment from 'moment'; import { i18next } from '../base/i18n'; import { parseURIString } from '../base/util'; -import { RECENT_URL_STORAGE } from './constants'; - /** * MomentJS uses static language bundle loading, so in order to support dynamic * language selection in the app we need to load all bundles that we support in @@ -36,76 +34,43 @@ require('moment/locale/tr'); require('moment/locale/zh-cn'); /** - * Retreives the recent room list and generates all the data needed to be + * Retrieves the recent room list and generates all the data needed to be * displayed. * - * @returns {Promise} The {@code Promise} to be resolved when the list is - * available. + * @param {Array} list - The stored recent list retrieved from Redux. + * @returns {Array} */ -export function getRecentRooms(): Promise> { - return new Promise((resolve, reject) => - window.localStorage._getItemAsync(RECENT_URL_STORAGE).then( - /* onFulfilled */ recentURLs => { - const recentRoomDS = []; +export function getRecentRooms(list: Array): Array { + const recentRoomDS = []; - if (recentURLs) { - // We init the locale on every list render, so then it - // changes immediately if a language change happens in the - // app. - const locale = _getSupportedLocale(); + if (list.length) { + // We init the locale on every list render, so then it changes + // immediately if a language change happens in the app. + const locale = _getSupportedLocale(); - for (const e of JSON.parse(recentURLs)) { - const location = parseURIString(e.conference); + for (const e of list) { + const location = parseURIString(e.conference); - if (location && location.room && location.hostname) { - recentRoomDS.push({ - baseURL: - `${location.protocol}//${location.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 - }); - } - } - } + if (location && location.room && location.hostname) { + recentRoomDS.push({ + baseURL: `${location.protocol}//${location.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 + }); + } + } + } - resolve(recentRoomDS.reverse()); - }, - /* onRejected */ reject) - ); -} - -/** - * Retreives the recent URL list as a list of objects. - * - * @returns {Array} The list of already stored recent URLs. - */ -export function getRecentURLs() { - const recentURLs = window.localStorage.getItem(RECENT_URL_STORAGE); - - return recentURLs ? JSON.parse(recentURLs) : []; -} - -/** - * Updates the recent URL list. - * - * @param {Array} recentURLs - The new URL list. - * @returns {void} - */ -export function updateRecentURLs(recentURLs: Array) { - window.localStorage.setItem( - RECENT_URL_STORAGE, - JSON.stringify(recentURLs) - ); + return recentRoomDS.reverse(); } /** @@ -142,8 +107,7 @@ function _getDateString(dateTimeStamp: number, locale: string) { * @returns {string} */ function _getDurationString(duration: number, locale: string) { - return _getLocalizedFormatter(duration, locale) - .humanize(); + return _getLocalizedFormatter(duration, locale).humanize(); } /** @@ -158,22 +122,23 @@ function _getInitials(room: string) { } /** - * Returns a localized date formatter initialized with the - * provided date (@code Date) or duration (@code Number). + * Returns a localized date formatter initialized with the provided date + * (@code Date) or duration (@code number). * * @private - * @param {Date | number} dateToFormat - 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: This * locale must be supported by the formatter so ensure this prerequisite before * invoking the function. * @returns {Object} */ -function _getLocalizedFormatter(dateToFormat: Date | number, locale: string) { - if (typeof dateToFormat === 'number') { - return moment.duration(dateToFormat).locale(locale); - } +function _getLocalizedFormatter(dateOrDuration: Date | number, locale: string) { + const m + = typeof dateOrDuration === 'number' + ? moment.duration(dateOrDuration) + : moment(dateOrDuration); - return moment(dateToFormat).locale(locale); + return m.locale(locale); } /** diff --git a/react/features/recent-list/index.js b/react/features/recent-list/index.js index 7ce19e0e6..daaa5d02a 100644 --- a/react/features/recent-list/index.js +++ b/react/features/recent-list/index.js @@ -1,3 +1,4 @@ export * from './components'; import './middleware'; +import './reducer'; diff --git a/react/features/recent-list/middleware.js b/react/features/recent-list/middleware.js index a5f1562ac..9f9815620 100644 --- a/react/features/recent-list/middleware.js +++ b/react/features/recent-list/middleware.js @@ -3,8 +3,7 @@ import { CONFERENCE_WILL_LEAVE, SET_ROOM } from '../base/conference'; import { MiddlewareRegistry } from '../base/redux'; -import { LIST_SIZE } from './constants'; -import { getRecentURLs, updateRecentURLs } from './functions'; +import { storeCurrentConference, updateConferenceDuration } from './actions'; /** * Middleware that captures joined rooms so they can be saved into @@ -16,93 +15,47 @@ import { getRecentURLs, updateRecentURLs } from './functions'; MiddlewareRegistry.register(store => next => action => { switch (action.type) { case CONFERENCE_WILL_LEAVE: - return _updateConferenceDuration(store, next, action); + _updateConferenceDuration(store, next); + break; case SET_ROOM: - return _storeJoinedRoom(store, next, action); + _maybeStoreCurrentConference(store, next, action); + break; } return next(action); }); /** - * Stores the recently joined room into {@code window.localStorage}. + * Checks if there is a current conference (upon SET_ROOM action), and saves it + * if necessary. * - * @param {Store} store - The redux store in which the specified action is being - * dispatched. - * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the - * specified action to the specified store. - * @param {Action} action - The redux action {@code SET_ROOM} which is being - * dispatched in the specified store. + * @param {Store} store - The redux store. + * @param {Dispatch} next - The redux {@code dispatch} function. + * @param {Action} action - The redux action. * @private - * @returns {Object} The new state that is the result of the reduction of the - * specified action. + * @returns {void} */ -function _storeJoinedRoom(store, next, action) { - const result = next(action); - +function _maybeStoreCurrentConference(store, next, action) { + const { locationURL } = store.getState()['features/base/connection']; const { room } = action; if (room) { - const { locationURL } = store.getState()['features/base/connection']; - const conference = locationURL.href; - - // If the current conference is already in the list, we remove it to add - // it to the top at the end. - const recentURLs - = getRecentURLs() - .filter(e => e.conference !== conference); - - // XXX This is a reverse sorted array (i.e. newer elements at the end). - recentURLs.push({ - conference, - conferenceDuration: 0, - date: Date.now() - }); - - // maximising the size - recentURLs.splice(0, recentURLs.length - LIST_SIZE); - - updateRecentURLs(recentURLs); + next(storeCurrentConference(locationURL)); } - - return result; } /** - * Updates the conference length when left. + * Updates the duration of the last conference stored in the list. * - * @param {Store} store - The redux store in which the specified action is being - * dispatched. - * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the - * specified action to the specified store. - * @param {Action} action - The redux action {@code CONFERENCE_WILL_LEAVE} which - * is being dispatched in the specified store. + * @param {Store} store - The redux store. + * @param {Dispatch} next - The redux {@code dispatch} function. + * @param {Action} action - The redux action. * @private - * @returns {Object} The new state that is the result of the reduction of the - * specified action. + * @returns {void} */ -function _updateConferenceDuration({ getState }, next, action) { - const result = next(action); +function _updateConferenceDuration(store, next) { + const { locationURL } = store.getState()['features/base/connection']; - const { locationURL } = getState()['features/base/connection']; - - if (locationURL && locationURL.href) { - const recentURLs = getRecentURLs(); - - if (recentURLs.length > 0) { - const mostRecentURL = recentURLs[recentURLs.length - 1]; - - if (mostRecentURL.conference === locationURL.href) { - // The last conference start was stored so we need to update the - // length. - mostRecentURL.conferenceDuration - = Date.now() - mostRecentURL.date; - - updateRecentURLs(recentURLs); - } - } - } - - return result; + next(updateConferenceDuration(locationURL)); } diff --git a/react/features/recent-list/reducer.js b/react/features/recent-list/reducer.js new file mode 100644 index 000000000..d80ff7d85 --- /dev/null +++ b/react/features/recent-list/reducer.js @@ -0,0 +1,106 @@ +// @flow + +import { + STORE_CURRENT_CONFERENCE, + UPDATE_CONFERENCE_DURATION +} from './actionTypes'; +import { LIST_SIZE } from './constants'; + +import { PersistencyRegistry, ReducerRegistry } from '../base/redux'; + +/** + * The initial state of this feature. + */ +const DEFAULT_STATE = { + list: [] +}; + +/** + * The Redux subtree of this feature. + */ +const STORE_NAME = 'features/recent-list'; + +/** + * Registers the redux store subtree of this feature for persistency. + */ +PersistencyRegistry.register(STORE_NAME, { + list: true +}); + +/** + * Reduces the Redux actions of the feature features/recent-list. + */ +ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => { + switch (action.type) { + case STORE_CURRENT_CONFERENCE: + return _storeCurrentConference(state, action); + case UPDATE_CONFERENCE_DURATION: + return _updateConferenceDuration(state, action); + default: + return state; + } +}); + +/** +* 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; + 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 + = state.list + .filter(e => e.conference !== conference); + + // This is a reverse sorted array (i.e. newer elements at the end). + list.push({ + conference, + conferenceDuration: 0, // we don't have this data yet + date: Date.now() + }); + + // maximising the size + list.splice(0, list.length - LIST_SIZE); + + return { + list + }; +} + +/** + * Updates the conference length when left. + * + * @param {Object} state - The redux state. + * @param {Object} action - The redux action. + * @returns {Object} + */ +function _updateConferenceDuration(state, action) { + const { locationURL } = action; + + if (locationURL && locationURL.href) { + const list = state.list; + + if (list.length > 0) { + const mostRecentURL = list[list.length - 1]; + + if (mostRecentURL.conference === locationURL.href) { + // The last conference start was stored so we need to update the + // length. + mostRecentURL.conferenceDuration + = Date.now() - mostRecentURL.date; + + return { + list + }; + } + } + } + + return state; +}