diff --git a/react/features/app/components/AbstractApp.js b/react/features/app/components/AbstractApp.js index 9b3ebb6982ec..af7991cbb6cb 100644 --- a/react/features/app/components/AbstractApp.js +++ b/react/features/app/components/AbstractApp.js @@ -10,7 +10,11 @@ import Thunk from 'redux-thunk'; import { i18next } from '../../base/i18n'; import { localParticipantLeft } from '../../base/participants'; import { Fragment, RouteRegistry } from '../../base/react'; -import { MiddlewareRegistry, ReducerRegistry } from '../../base/redux'; +import { + MiddlewareRegistry, + ReducerRegistry, + StateListenerRegistry +} from '../../base/redux'; import { SoundCollection } from '../../base/sounds'; import { PersistenceRegistry } from '../../base/storage'; import { toURLString } from '../../base/util'; @@ -404,6 +408,9 @@ export class AbstractApp extends Component { } } + // StateListenerRegistry + store && StateListenerRegistry.subscribe(store); + return store; } diff --git a/react/features/base/redux/StateListenerRegistry.js b/react/features/base/redux/StateListenerRegistry.js new file mode 100644 index 000000000000..7bfa273eb326 --- /dev/null +++ b/react/features/base/redux/StateListenerRegistry.js @@ -0,0 +1,159 @@ +// @flow + +const logger = require('jitsi-meet-logger').getLogger(__filename); + +/** + * The type listener supported for registration with + * {@link StateListenerRegistry} in association with a {@link Selector}. + * + * @param {any} selection - The value derived from the redux store/state by the + * associated {@code Selector}. Immutable! + * @param {Store} store - The redux store. Provided in case the {@code Listener} + * needs to {@code dispatch} or {@code getState}. The latter is advisable only + * if the {@code Listener} is not to respond to changes to that state. + * @param {any} prevSelection - The value previously derived from the redux + * store/state by the associated {@code Selector}. The {@code Listener} is + * invoked only if {@code prevSelection} and {@code selection} are different. + * Immutable! + */ +type Listener = (selection: any, store: Store, prevSelection: any) => void; + +/** + * The type selector supported for registration with + * {@link StateListenerRegistry} in association with a {@link Listener}. + * + * @param {Object} state - The redux state from which the {@code Selector} is to + * derive data. + * @param {any} prevSelection - The value previously derived from the redux + * store/state by the {@code Selector}. Provided in case the {@code Selector} + * needs to derive the returned value from the specified {@code state} and + * {@code prevSelection}. Immutable! + * @returns {any} The value derived from the specified {@code state} and/or + * {@code prevSelection}. The associated {@code Listener} will only be invoked + * if the returned value is other than {@code prevSelection}. + */ +type Selector = (state: Object, prevSelection: any) => any; + +/** + * A type of a {@link Selector}-{@link Listener} association in which the + * {@code Listener} listens to changes in the values derived from a redux + * store/state by the {@code Selector}. + */ +type SelectorListener = { + + /** + * The {@code Listener} which listens to changes in the values selected by + * {@link selector}. + */ + listener: Listener, + + /** + * The {@code Selector} which selects values whose changes are listened to + * by {@link listener}. + */ + selector: Selector +}; + +/** + * A registry listeners which listen to changes in a redux store/state. + */ +class StateListenerRegistry { + /** + * The {@link Listener}s registered with this {@code StateListenerRegistry} + * to be notified when the values derived by associated {@link Selector}s + * from a redux store/state change. + */ + _selectorListeners: Set = new Set(); + + _listener: (Store) => void; + + /** + * Invoked by a specific redux store any time an action is dispatched, and + * some part of the state (tree) may potentially have changed. + * + * @param {Object} context - The redux store invoking the listener and the + * private state of this {@code StateListenerRegistry} associated with the + * redux store. + * @returns {void} + */ + _listener({ prevSelections, store }: { + prevSelections: Map, + store: Store + }) { + for (const selectorListener of this._selectorListeners) { + const prevSelection = prevSelections.get(selectorListener); + + try { + const selection + = selectorListener.selector( + store.getState(), + prevSelection); + + if (prevSelection !== selection) { + prevSelections.set(selectorListener, selection); + selectorListener.listener(selection, store, prevSelection); + } + } catch (e) { + // Don't let one faulty listener prevent other listeners from + // being notified about their associated changes. + logger.error(e); + } + } + } + + /** + * Registers a specific listener to be notified when the value derived by a + * specific {@code selector} from a redux store/state changes. + * + * @param {Function} selector - The pure {@code Function} of the redux + * store/state (and the previous selection of made by {@code selector}) + * which selects the value listened to by the specified {@code listener}. + * @param {Function} listener - The listener to register with this + * {@code StateListenerRegistry} so that it gets invoked when the value + * returned by the specified {@code selector} changes. + * @returns {void} + */ + register(selector: Selector, listener: Listener) { + this._selectorListeners.add({ + listener, + selector + }); + } + + /** + * Subscribes to a specific redux store (so that this instance gets notified + * any time an action is dispatched, and some part of the state (tree) of + * the specified redux store may potentially have changed). + * + * @param {Store} store - The redux store to which this + * {@code StateListenerRegistry} is to {@code subscribe}. + * @returns {void} + */ + subscribe(store: Store) { + // XXX If StateListenerRegistry is not utilized by the app to listen to + // state changes, do not bother subscribing to the store at all. + if (this._selectorListeners.size) { + store.subscribe( + this._listener.bind( + this, + { + /** + * The previous selections of the {@code Selector}s + * registered with this {@code StateListenerRegistry}. + * + * @type Map + */ + prevSelections: new Map(), + + /** + * The redux store. + * + * @type Store + */ + store + })); + } + } +} + +export default new StateListenerRegistry(); diff --git a/react/features/base/redux/index.js b/react/features/base/redux/index.js index 9658a8706979..1643344a88e0 100644 --- a/react/features/base/redux/index.js +++ b/react/features/base/redux/index.js @@ -1,3 +1,4 @@ export * from './functions'; export { default as MiddlewareRegistry } from './MiddlewareRegistry'; export { default as ReducerRegistry } from './ReducerRegistry'; +export { default as StateListenerRegistry } from './StateListenerRegistry';