This commit is contained in:
26
react/features/mobile/watchos/actionTypes.ts
Normal file
26
react/features/mobile/watchos/actionTypes.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* See {@link setConferenceTimestamp} for more details.
|
||||
* {
|
||||
* type: SET_CONFERENCE_TIMESTAMP,
|
||||
* conferenceTimestamp: number
|
||||
* }
|
||||
*/
|
||||
export const SET_CONFERENCE_TIMESTAMP = Symbol('WATCH_OS_SET_CONFERENCE_TIMESTAMP');
|
||||
|
||||
/**
|
||||
* See {@link setSessionId} action for more details.
|
||||
* {
|
||||
* type: SET_SESSION_ID,
|
||||
* sessionID: number
|
||||
* }
|
||||
*/
|
||||
export const SET_SESSION_ID = Symbol('WATCH_OS_SET_SESSION_ID');
|
||||
|
||||
/**
|
||||
* See {@link setWatchReachable} for more details.
|
||||
* {
|
||||
* type: SET_WATCH_REACHABLE,
|
||||
* watchReachable: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_WATCH_REACHABLE = Symbol('WATCH_OS_SET_WATCH_REACHABLE');
|
||||
53
react/features/mobile/watchos/actions.ts
Normal file
53
react/features/mobile/watchos/actions.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { SET_CONFERENCE_TIMESTAMP, SET_SESSION_ID, SET_WATCH_REACHABLE } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Stores a timestamp when the conference is joined, so that the watch counterpart can start counting from when
|
||||
* the meeting has really started.
|
||||
*
|
||||
* @param {number} conferenceTimestamp - A timestamp retrieved with {@code newDate.getTime()}.
|
||||
* @returns {{
|
||||
* type: SET_CONFERENCE_TIMESTAMP,
|
||||
* conferenceTimestamp: number
|
||||
* }}
|
||||
*/
|
||||
export function setConferenceTimestamp(conferenceTimestamp: number) {
|
||||
return {
|
||||
type: SET_CONFERENCE_TIMESTAMP,
|
||||
conferenceTimestamp
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the session ID which is sent to the Watch app and then used by the app to send commands. Commands from
|
||||
* the watch are accepted only if the 'sessionID' passed by the Watch matches the one currently stored in Redux. It is
|
||||
* supposed to prevent from processing outdated commands.
|
||||
*
|
||||
* @returns {{
|
||||
* type: SET_SESSION_ID,
|
||||
* sessionID: number
|
||||
* }}
|
||||
*/
|
||||
export function setSessionId() {
|
||||
return {
|
||||
type: SET_SESSION_ID,
|
||||
sessionID: new Date().getTime()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the reachable status of the watch. It's used to get in sync with the watch counterpart when it gets
|
||||
* reconnected, but also to prevent from sending updates if the app is not installed at all (which would fail with
|
||||
* an error).
|
||||
*
|
||||
* @param {boolean} isReachable - Indicates whether the watch is currently reachable or not.
|
||||
* @returns {{
|
||||
* type: SET_WATCH_REACHABLE,
|
||||
* watchReachable: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setWatchReachable(isReachable: boolean) {
|
||||
return {
|
||||
type: SET_WATCH_REACHABLE,
|
||||
watchReachable: isReachable
|
||||
};
|
||||
}
|
||||
9
react/features/mobile/watchos/constants.ts
Normal file
9
react/features/mobile/watchos/constants.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// NOTE When changing any of the commands make sure to update JitsiMeetCommands enum in the WatchKit extension code.
|
||||
|
||||
export const CMD_HANG_UP = 'hangup';
|
||||
|
||||
export const CMD_JOIN_CONFERENCE = 'joinConference';
|
||||
|
||||
export const CMD_SET_MUTED = 'setMuted';
|
||||
|
||||
export const MAX_RECENT_URLS = 10;
|
||||
3
react/features/mobile/watchos/logger.ts
Normal file
3
react/features/mobile/watchos/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../../base/logging/functions';
|
||||
|
||||
export default getLogger('features/mobile/watchos');
|
||||
191
react/features/mobile/watchos/middleware.ts
Normal file
191
react/features/mobile/watchos/middleware.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { NativeModules, Platform } from 'react-native';
|
||||
import { updateApplicationContext, watchEvents } from 'react-native-watch-connectivity';
|
||||
|
||||
import { appNavigate } from '../../app/actions';
|
||||
import { IStore } from '../../app/types';
|
||||
import { APP_WILL_MOUNT } from '../../base/app/actionTypes';
|
||||
import { IStateful } from '../../base/app/types';
|
||||
import { CONFERENCE_JOINED } from '../../base/conference/actionTypes';
|
||||
import { getCurrentConferenceUrl } from '../../base/connection/functions';
|
||||
import { setAudioMuted } from '../../base/media/actions';
|
||||
import MiddlewareRegistry from '../../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../../base/redux/StateListenerRegistry';
|
||||
import { toState } from '../../base/redux/functions';
|
||||
|
||||
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 { AppInfo } = NativeModules;
|
||||
const watchOSEnabled = Platform.OS === 'ios' && !AppInfo.isLiteSDK;
|
||||
|
||||
// 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 }: IStore) {
|
||||
watchEvents.addListener('reachability', reachable => {
|
||||
dispatch(setWatchReachable(reachable));
|
||||
_updateApplicationContext(getState);
|
||||
});
|
||||
|
||||
watchEvents.addListener('message', message => {
|
||||
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: any = 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: IStateful) {
|
||||
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: IStateful) {
|
||||
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: IStateful) {
|
||||
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: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const { conferenceTimestamp, sessionID, watchReachable } = state['features/mobile/watchos'];
|
||||
|
||||
if (!watchReachable) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
updateApplicationContext({
|
||||
conferenceTimestamp,
|
||||
conferenceURL: getCurrentConferenceUrl(state),
|
||||
micMuted: _isAudioMuted(state),
|
||||
recentURLs: _getRecentUrls(state),
|
||||
sessionID
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to stringify or send the context', error);
|
||||
}
|
||||
}
|
||||
41
react/features/mobile/watchos/reducer.ts
Normal file
41
react/features/mobile/watchos/reducer.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import ReducerRegistry from '../../base/redux/ReducerRegistry';
|
||||
import { assign } from '../../base/redux/functions';
|
||||
|
||||
import { SET_CONFERENCE_TIMESTAMP, SET_SESSION_ID, SET_WATCH_REACHABLE } from './actionTypes';
|
||||
|
||||
export interface IMobileWatchOSState {
|
||||
conferenceTimestamp?: number;
|
||||
sessionID: number;
|
||||
watchReachable?: boolean;
|
||||
}
|
||||
|
||||
const INITIAL_STATE = {
|
||||
sessionID: new Date().getTime()
|
||||
};
|
||||
|
||||
/**
|
||||
* Reduces the Redux actions of the feature features/mobile/watchos.
|
||||
*/
|
||||
ReducerRegistry.register<IMobileWatchOSState>('features/mobile/watchos',
|
||||
(state = INITIAL_STATE, action): IMobileWatchOSState => {
|
||||
switch (action.type) {
|
||||
case SET_CONFERENCE_TIMESTAMP: {
|
||||
return assign(state, {
|
||||
conferenceTimestamp: action.conferenceTimestamp
|
||||
});
|
||||
}
|
||||
case SET_SESSION_ID: {
|
||||
return assign(state, {
|
||||
sessionID: action.sessionID,
|
||||
conferenceTimestamp: 0
|
||||
});
|
||||
}
|
||||
case SET_WATCH_REACHABLE: {
|
||||
return assign(state, {
|
||||
watchReachable: action.watchReachable
|
||||
});
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user