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.
199 lines
6.0 KiB
JavaScript
199 lines
6.0 KiB
JavaScript
// @flow
|
|
|
|
import { Platform } from 'react-native';
|
|
import * as watch from 'react-native-watch-connectivity';
|
|
|
|
import { appNavigate } from '../../app/actions';
|
|
import { APP_WILL_MOUNT } from '../../base/app';
|
|
import { CONFERENCE_JOINED } from '../../base/conference';
|
|
import { getCurrentConferenceUrl } from '../../base/connection';
|
|
import { setAudioMuted } from '../../base/media';
|
|
import {
|
|
MiddlewareRegistry,
|
|
StateListenerRegistry,
|
|
toState
|
|
} from '../../base/redux';
|
|
|
|
import { setConferenceTimestamp, setSessionId, setWatchReachable } from './actions';
|
|
import { CMD_HANG_UP, CMD_JOIN_CONFERENCE, CMD_SET_MUTED, MAX_RECENT_URLS } from './constants';
|
|
import logger from './logger';
|
|
|
|
const watchOSEnabled = Platform.OS === 'ios';
|
|
|
|
// Handles the recent URLs state sent to the watch
|
|
watchOSEnabled && StateListenerRegistry.register(
|
|
/* selector */ state => state['features/recent-list'],
|
|
/* listener */ (recentListState, { getState }) => {
|
|
_updateApplicationContext(getState);
|
|
});
|
|
|
|
// Handles the mic muted state sent to the watch
|
|
watchOSEnabled && StateListenerRegistry.register(
|
|
/* selector */ state => _isAudioMuted(state),
|
|
/* listener */ (isAudioMuted, { getState }) => {
|
|
_updateApplicationContext(getState);
|
|
});
|
|
|
|
// Handles the conference URL state sent to the watch
|
|
watchOSEnabled && StateListenerRegistry.register(
|
|
/* selector */ state => getCurrentConferenceUrl(state),
|
|
/* listener */ (currentUrl, { dispatch, getState }) => {
|
|
dispatch(setSessionId());
|
|
_updateApplicationContext(getState);
|
|
});
|
|
|
|
/**
|
|
* Middleware that captures conference actions.
|
|
*
|
|
* @param {Store} store - The redux store.
|
|
* @returns {Function}
|
|
*/
|
|
watchOSEnabled && MiddlewareRegistry.register(store => next => action => {
|
|
switch (action.type) {
|
|
case APP_WILL_MOUNT:
|
|
_appWillMount(store);
|
|
break;
|
|
case CONFERENCE_JOINED:
|
|
store.dispatch(setConferenceTimestamp(new Date().getTime()));
|
|
_updateApplicationContext(store.getState());
|
|
break;
|
|
}
|
|
|
|
return next(action);
|
|
});
|
|
|
|
/**
|
|
* Registers listeners to the react-native-watch-connectivity lib.
|
|
*
|
|
* @param {Store} store - The redux store.
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
function _appWillMount({ dispatch, getState }) {
|
|
watch.subscribeToWatchReachability((error, reachable) => {
|
|
dispatch(setWatchReachable(reachable));
|
|
_updateApplicationContext(getState);
|
|
});
|
|
|
|
watch.subscribeToMessages((error, message) => {
|
|
if (error) {
|
|
logger.error('watch.subscribeToMessages error:', error);
|
|
|
|
return;
|
|
}
|
|
|
|
const {
|
|
command,
|
|
sessionID
|
|
} = message;
|
|
const currentSessionID = _getSessionId(getState());
|
|
|
|
if (!sessionID || sessionID !== currentSessionID) {
|
|
logger.warn(
|
|
`Ignoring outdated watch command: ${message.command}`
|
|
+ ` sessionID: ${sessionID} current session ID: ${currentSessionID}`);
|
|
|
|
return;
|
|
}
|
|
|
|
switch (command) {
|
|
case CMD_HANG_UP:
|
|
if (typeof getCurrentConferenceUrl(getState()) !== undefined) {
|
|
dispatch(appNavigate(undefined));
|
|
}
|
|
break;
|
|
case CMD_JOIN_CONFERENCE: {
|
|
const newConferenceURL = message.data;
|
|
const oldConferenceURL = getCurrentConferenceUrl(getState());
|
|
|
|
if (oldConferenceURL !== newConferenceURL) {
|
|
dispatch(appNavigate(newConferenceURL));
|
|
}
|
|
break;
|
|
}
|
|
case CMD_SET_MUTED:
|
|
dispatch(
|
|
setAudioMuted(
|
|
message.muted === 'true',
|
|
/* ensureTrack */ true));
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Gets the current Apple Watch session's ID. A new session is started whenever the conference URL has changed. It is
|
|
* used to filter out outdated commands which may arrive very later if the Apple Watch loses the connectivity.
|
|
*
|
|
* @param {Object|Function} stateful - Either the whole Redux state object or the Redux store's {@code getState} method.
|
|
* @returns {number}
|
|
* @private
|
|
*/
|
|
function _getSessionId(stateful) {
|
|
const state = toState(stateful);
|
|
|
|
return state['features/mobile/watchos'].sessionID;
|
|
}
|
|
|
|
/**
|
|
* Gets the list of recent URLs to be passed over to the Watch app.
|
|
*
|
|
* @param {Object|Function} stateful - Either the whole Redux state object or the Redux store's {@code getState} method.
|
|
* @returns {Array<Object>}
|
|
* @private
|
|
*/
|
|
function _getRecentUrls(stateful) {
|
|
const state = toState(stateful);
|
|
const recentURLs = state['features/recent-list'];
|
|
|
|
// Trim to MAX_RECENT_URLS and reverse the list
|
|
const reversedList = recentURLs.slice(-MAX_RECENT_URLS);
|
|
|
|
reversedList.reverse();
|
|
|
|
return reversedList;
|
|
}
|
|
|
|
/**
|
|
* Determines the audio muted state to be sent to the apple watch.
|
|
*
|
|
* @param {Object|Function} stateful - Either the whole Redux state object or the Redux store's {@code getState} method.
|
|
* @returns {boolean}
|
|
* @private
|
|
*/
|
|
function _isAudioMuted(stateful) {
|
|
const state = toState(stateful);
|
|
const { audio } = state['features/base/media'];
|
|
|
|
return audio.muted;
|
|
}
|
|
|
|
/**
|
|
* Sends the context to the watch os app. At the time of this writing it's the entire state of
|
|
* the 'features/mobile/watchos' reducer.
|
|
*
|
|
* @param {Object|Function} stateful - Either the whole Redux state object or the Redux store's {@code getState} method.
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
function _updateApplicationContext(stateful) {
|
|
const state = toState(stateful);
|
|
const { conferenceTimestamp, sessionID, watchReachable } = state['features/mobile/watchos'];
|
|
|
|
if (!watchReachable) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
watch.updateApplicationContext({
|
|
conferenceTimestamp,
|
|
conferenceURL: getCurrentConferenceUrl(state),
|
|
micMuted: _isAudioMuted(state),
|
|
recentURLs: _getRecentUrls(state),
|
|
sessionID
|
|
});
|
|
} catch (error) {
|
|
logger.error('Failed to stringify or send the context', error);
|
|
}
|
|
}
|