init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View 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');

View 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
};
}

View 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;

View File

@@ -0,0 +1,3 @@
import { getLogger } from '../../base/logging/functions';
export default getLogger('features/mobile/watchos');

View 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);
}
}

View 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;
}
});