Saúl Ibarra Corretgé 6e679f952f redux: refactor loading of middlewares and reducers
Up until now we relied on implicit loading of middlewares and reducers, through
having imports in each feature's index.js.

This leads to many complex import cycles which result in (sometimes) hard to fix
bugs in addition to (often) breaking mobile because a web-only feature gets
imported on mobile too, thanks to the implicit loading.

This PR changes that to make the process explicit. Both middlewares and reducers
are imported in a single place, the app entrypoint. They have been divided into
3 categories: any, web and native, which represent each of the platforms
  respectively.

Ideally no feature should have an index.js exporting actions, action types and
components, but that's a larger ordeal, so this is just the first step in
getting there. In order to both set example and avoid large cycles the app
feature has been refactored to not have an idex.js itself.
2020-06-16 11:24:15 +02:00

178 lines
4.9 KiB
JavaScript

// @flow
import type { Dispatch } from 'redux';
import { appNavigate } from '../app/actions';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT
} from '../base/conference';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../base/connection';
import { hideDialog, isDialogOpen } from '../base/dialog';
import {
JitsiConferenceErrors,
JitsiConnectionErrors
} from '../base/lib-jitsi-meet';
import { MiddlewareRegistry } from '../base/redux';
import {
CANCEL_LOGIN,
STOP_WAIT_FOR_OWNER,
WAIT_FOR_OWNER
} from './actionTypes';
import {
_openLoginDialog,
_openWaitForOwnerDialog,
stopWaitForOwner,
waitForOwner
} from './actions';
import { LoginDialog, WaitForOwnerDialog } from './components';
/**
* Middleware that captures connection or conference failed errors and controls
* {@link WaitForOwnerDialog} and {@link LoginDialog}.
*
* FIXME Some of the complexity was introduced by the lack of dialog stacking.
*
* @param {Store} store - Redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case CANCEL_LOGIN: {
const { dispatch, getState } = store;
const { thenableWithCancel } = getState()['features/authentication'];
thenableWithCancel && thenableWithCancel.cancel();
// The LoginDialog can be opened on top of "wait for owner". The app
// should navigate only if LoginDialog was open without the
// WaitForOwnerDialog.
if (!isDialogOpen(store, WaitForOwnerDialog)) {
if (_isWaitingForOwner(store)) {
// Instead of hiding show the new one.
const result = next(action);
dispatch(_openWaitForOwnerDialog());
return result;
}
// Go back to the app's entry point.
_hideLoginDialog(store);
// FIXME Like cancelWaitForOwner, dispatch conferenceLeft to notify
// the external-api.
dispatch(appNavigate(undefined));
}
break;
}
case CONFERENCE_FAILED: {
const { error } = action;
// XXX The feature authentication affords recovery from
// CONFERENCE_FAILED caused by
// JitsiConferenceErrors.AUTHENTICATION_REQUIRED.
let recoverable;
if (error.name === JitsiConferenceErrors.AUTHENTICATION_REQUIRED) {
if (typeof error.recoverable === 'undefined') {
error.recoverable = true;
}
recoverable = error.recoverable;
}
if (recoverable) {
store.dispatch(waitForOwner());
} else {
store.dispatch(stopWaitForOwner());
}
break;
}
case CONFERENCE_JOINED:
if (_isWaitingForOwner(store)) {
store.dispatch(stopWaitForOwner());
}
_hideLoginDialog(store);
break;
case CONFERENCE_LEFT:
store.dispatch(stopWaitForOwner());
break;
case CONNECTION_ESTABLISHED:
_hideLoginDialog(store);
break;
case CONNECTION_FAILED: {
const { error } = action;
if (error
&& error.name === JitsiConnectionErrors.PASSWORD_REQUIRED
&& typeof error.recoverable === 'undefined') {
error.recoverable = true;
store.dispatch(_openLoginDialog());
}
break;
}
case STOP_WAIT_FOR_OWNER:
_clearExistingWaitForOwnerTimeout(store);
store.dispatch(hideDialog(WaitForOwnerDialog));
break;
case WAIT_FOR_OWNER: {
_clearExistingWaitForOwnerTimeout(store);
const { handler, timeoutMs } = action;
action.waitForOwnerTimeoutID = setTimeout(handler, timeoutMs);
// The WAIT_FOR_OWNER action is cyclic and we don't want to hide the
// login dialog every few seconds.
isDialogOpen(store, LoginDialog)
|| store.dispatch(_openWaitForOwnerDialog());
break;
}
}
return next(action);
});
/**
* Will clear the wait for conference owner timeout handler if any is currently
* set.
*
* @param {Object} store - The redux store.
* @returns {void}
*/
function _clearExistingWaitForOwnerTimeout(
{ getState }: { getState: Function }) {
const { waitForOwnerTimeoutID } = getState()['features/authentication'];
waitForOwnerTimeoutID && clearTimeout(waitForOwnerTimeoutID);
}
/**
* Hides {@link LoginDialog} if it's currently displayed.
*
* @param {Object} store - The redux store.
* @returns {void}
*/
function _hideLoginDialog({ dispatch }: { dispatch: Dispatch<any> }) {
dispatch(hideDialog(LoginDialog));
}
/**
* Checks if the cyclic "wait for conference owner" task is currently scheduled.
*
* @param {Object} store - The redux store.
* @returns {boolean}
*/
function _isWaitingForOwner({ getState }: { getState: Function }) {
return Boolean(getState()['features/authentication'].waitForOwnerTimeoutID);
}