Compare commits

...

7 Commits

Author SHA1 Message Date
paweldomas c2191e3a28 callkit with base/session 2018-08-07 12:20:38 -05:00
paweldomas 72e3e8593d feat(base/session): store 'room' in the session
Stores name of the conference room in the session when it's being
created.
2018-08-07 12:20:38 -05:00
paweldomas 67a8b4915d feat(base/session): add SESSION_CONFIGURED event
The SESSION_CONFIGURED event is fired once the config has been set,
after either being loaded or restored from the storage.
2018-08-07 12:20:38 -05:00
paweldomas 468d4a7150 ref(mobile/external-api): use base/session 2018-08-07 12:20:38 -05:00
paweldomas 2a01e29fec feat: add features/base/session 2018-08-07 12:20:38 -05:00
paweldomas 90a64d30dc ref(base/config): keep 'locationURL' after SET_CONFIG
This is required for the session feature to be able to tell what's
the latest URL the app is working with.
2018-08-07 12:20:38 -05:00
paweldomas 31905d4f63 debug actions 2018-08-07 12:20:38 -05:00
12 changed files with 770 additions and 256 deletions

View File

@ -37,6 +37,9 @@ const INITIAL_RN_STATE = {
// fastest to merely disable them.
disableAudioLevels: true,
// FIXME flow complains about missing 'locationURL' missing in _setConfig
locationURL: undefined,
p2p: {
disableH264: false,
preferH264: true
@ -126,8 +129,10 @@ function _setConfig(state, { config }) {
const newState = _.merge(
{},
config,
{ error: undefined },
config, {
error: undefined,
locationURL: state.locationURL
},
// The config of _getInitialState() is meant to override the config
// downloaded from the Jitsi Meet deployment because the former contains

View File

@ -0,0 +1,14 @@
/**
* FIXME.
*
* {
* type: SET_SESSION,
* session: {
* url: {string},
* state: {string},
* ...data
* }
* }
* @public
*/
export const SET_SESSION = Symbol('SET_SESSION');

View File

@ -0,0 +1,16 @@
import { SET_SESSION } from './actionTypes';
/**
* FIXME.
*
* @param {string} session - FIXME.
* @returns {{
* type: SET_SESSION
* }}
*/
export function setSession(session) {
return {
type: SET_SESSION,
session
};
}

View File

@ -0,0 +1,12 @@
export const SESSION_CONFIGURED = Symbol('SESSION_CONFIGURED');
export const SESSION_ENDED = Symbol('SESSION_ENDED');
export const SESSION_FAILED = Symbol('SESSION_FAILED');
export const SESSION_STARTED = Symbol('SESSION_STARTED');
export const SESSION_WILL_END = Symbol('SESSION_WILL_END');
export const SESSION_WILL_START = Symbol('SESSION_WILL_START');

View File

@ -0,0 +1,36 @@
// @flow
import { toState } from '../redux';
import { toURLString } from '../util';
/**
* FIXME.
*
* @param {Function|Object} stateful - FIXME.
* @param {string} url - FIXME.
* @returns {*}
*/
export function getSession(stateful: Function | Object, url: string): ?Object {
const state = toState(stateful);
const session = state['features/base/session'].get(url);
if (!session) {
console.info(`SESSION NOT FOUND FOR URL: ${url}`);
}
return session;
}
/**
* FIXME.
*
* @param {Function | Object} stateful - FIXME.
* @returns {Object}
*/
export function getCurrentSession(stateful: Function | Object): ?Object {
const state = toState(stateful);
const { locationURL } = state['features/base/config'];
return getSession(state, toURLString(locationURL));
}

View File

@ -0,0 +1,7 @@
export * from './actions';
export * from './actionTypes';
export * from './constants';
export * from './functions';
import './middleware';
import './reducer';

View File

@ -0,0 +1,349 @@
// @flow
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN,
CONFERENCE_WILL_LEAVE,
JITSI_CONFERENCE_URL_KEY,
isRoomValid
} from '../../base/conference';
import {
CONNECTION_DISCONNECTED,
CONNECTION_FAILED,
CONNECTION_WILL_CONNECT
} from '../../base/connection';
import {
MiddlewareRegistry,
toState
} from '../../base/redux';
import { parseURIString, toURLString } from '../../base/util';
import {
SESSION_CONFIGURED,
SESSION_ENDED,
SESSION_FAILED,
SESSION_STARTED,
SESSION_WILL_END,
SESSION_WILL_START
} from './constants';
import { setSession } from './actions';
import { CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from '../config';
import { getCurrentSession, getSession } from './functions';
/**
* Middleware that maintains conference sessions. The features spans across
* three features strictly related to the conference lifecycle.
* The first one is the base/config which configures the session. It's
* 'locationURL' state is used to tell what's the current conference URL the app
* is working with. The session starts as soon as {@link CONFIG_WILL_LOAD} event
* arrives. The {@code locationURL} instance is stored in the session to
* associate the load config request with the session and be able to distinguish
* between the current and outdated load config request failures. After the
* config is loaded the lifecycle moves to the base/connection feature which
* creates a {@code JitsiConnection} and tries to connect to the server. On
* {@code CONNECTION_WILL_CONNECT} the connection instance is stored in the
* session and used later to filter the events similar to what's done for
* the load config requests. The base/conference feature adds the last part to
* the session's lifecycle. A {@code JitsiConference} instance is stored in the
* session on the {@code CONFERENCE_WILL_JOIN} action. A session is considered
* alive as long as either connection or conference is available and
* operational.
*
* @param {Store} store - Redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const result = next(action);
const { type } = action;
switch (type) {
case CONFERENCE_WILL_JOIN: {
const { conference } = action;
const { locationURL } = store.getState()['features/base/connection'];
const url = toURLString(locationURL);
const session = getSession(store, url);
if (session) {
store.dispatch(setSession({
url: session.url,
conference
}));
} else {
console.info(`IGNORED WILL_JOIN FOR: ${url}`);
}
break;
}
case CONFERENCE_JOINED: {
const { conference } = action;
const session = findSessionForConference(store, conference);
const state = session && session.state;
if (state === SESSION_CONFIGURED) {
store.dispatch(
setSession({
// Flow complains that the session can be undefined, but it
// can't if the state is defined.
// $FlowExpectedError
url: session.url,
state: SESSION_STARTED
}));
} else {
// eslint-disable-next-line max-len
console.info(`IGNORED CONF JOINED FOR: ${toURLString(conference[JITSI_CONFERENCE_URL_KEY])}`);
}
break;
}
case CONFERENCE_LEFT:
case CONFERENCE_FAILED: {
const { conference, error } = action;
const session = findSessionForConference(store, conference);
// FIXME update comments
// XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
// prevented the user from joining a specific conference but the app may
// be able to eventually join the conference. For example, the app will
// ask the user for a password upon
// JitsiConferenceErrors.PASSWORD_REQUIRED and will retry joining the
// conference afterwards. Such errors are to not reach the native
// counterpart of the External API (or at least not in the
// fatality/finality semantics attributed to
// conferenceFailed:/onConferenceFailed).
if (session) {
if (!error || isGameOver(store, session, error)) {
if (session.connection) {
store.dispatch(
setSession({
url: session.url,
conference: undefined
}));
} else {
store.dispatch(
setSession({
url: session.url,
state: error ? SESSION_FAILED : SESSION_ENDED,
error
}));
}
}
} else {
// eslint-disable-next-line max-len
console.info(`IGNORED FAILED/LEFT for ${toURLString(conference[JITSI_CONFERENCE_URL_KEY])}`, error);
}
break;
}
// NOTE WILL_JOIN is fired on SET_ROOM
// case CONFERENCE_WILL_JOIN:
case CONFERENCE_WILL_LEAVE: {
const { conference } = action;
const url = toURLString(conference[JITSI_CONFERENCE_URL_KEY]);
const session = findSessionForConference(store, conference);
const state = session && session.state;
if (state && state !== SESSION_WILL_END) {
store.dispatch(
setSession({
// Flow complains that the session can be undefined, but it
// can't if the state is defined.
// $FlowExpectedError
url: session.url,
state: SESSION_WILL_END
}));
} else {
console.info(`IGNORED WILL LEAVE FOR ${url}`);
}
break;
}
case CONNECTION_WILL_CONNECT: {
const { connection } = action;
const { locationURL } = store.getState()['features/base/connection'];
const url = toURLString(locationURL);
const session = getSession(store, url);
if (session) {
store.dispatch(
setSession({
url: session.url,
connection,
conference: undefined // Detach from the old conference
}));
} else {
console.info(`IGNORED CONNECTION_WILL_CONNECT FOR: ${url}`);
}
break;
}
case CONNECTION_DISCONNECTED:
case CONNECTION_FAILED: {
const { connection, error } = action;
const session = findSessionForConnection(store, connection);
if (session) {
// Remove connection from the session, but wait for
// the conference to be removed as well.
if (!error || isGameOver(store, session, error)) {
if (session.conference) {
store.dispatch(
setSession({
url: session.url,
connection: undefined
}));
} else {
store.dispatch(
setSession({
url: session.url,
state: error ? SESSION_FAILED : SESSION_ENDED,
error
}));
}
}
} else {
console.info('Ignored DISCONNECTED/FAILED for connection');
}
break;
}
case SET_CONFIG: {
// XXX SET_CONFIG IS ALWAYS RELEVANT
const { locationURL } = store.getState()['features/base/config'];
const url = toURLString(locationURL);
const session = getSession(store, url);
const state = session && session.state;
if (state === SESSION_WILL_START) {
store.dispatch(
setSession({
url,
state: SESSION_CONFIGURED
}));
}
break;
}
case CONFIG_WILL_LOAD: {
const { locationURL } = action;
const url = toURLString(locationURL);
const session = getSession(store, url);
// The back and forth to string conversion is here, because there's no
// guarantee that the locationURL will be the exact custom structure
// which contains the room property.
let { room } = parseURIString(url);
// Validate the room
room = isRoomValid(room) ? room : undefined;
if (room && !session) {
store.dispatch(
setSession({
url,
state: SESSION_WILL_START,
locationURL,
room
}));
} else if (room && session) {
// Update to the new locationURL instance
store.dispatch(
setSession({
url,
locationURL
}));
} else {
console.info(`IGNORED CFG WILL LOAD FOR ${url}`);
}
break;
}
case LOAD_CONFIG_ERROR: {
const { error, locationURL } = action;
const url = toURLString(locationURL);
const session = getSession(store, url);
if (session && session.locationURL === locationURL) {
if (isGameOver(store, session, error)) {
store.dispatch(
setSession({
url,
state: SESSION_FAILED,
error
}));
}
} else {
console.info(`IGNORED LOAD_CONFIG_ERROR FOR: ${url}`);
}
break;
}
}
return result;
});
/**
* FIXME A session is to be terminated either when the recoverable flag is
* explicitly set to {@code false} or if the error arrives for a session which
* is no longer current (the app has started working with another session).
* This can happen when a conference which is being disconnected fails in which
* case the session needs to be ended even if the flag is not {@code false}
* because we know that there's no fatal error handling. This is kind of
* a contract between the fatal error feature and the session which probably
* indicates that the fatal error detection and handling should be incorporated
* into the session feature.
*
* @param {Object | Function} stateful - FIXME.
* @param {Object} session - FIXME.
* @param {Object} error - FIXME.
* @returns {boolean}
*/
function isGameOver(stateful, session, error) {
return getCurrentSession(stateful) !== session
|| error.recoverable === false;
}
/**
* FIXME.
*
* @param {Object | Function} stateful - FIXME.
* @param {JitsiConnection} connection - FIXME.
* @returns {Object|undefined}
*/
function findSessionForConnection(stateful, connection) {
const state = toState(stateful);
for (const session of state['features/base/session'].values()) {
if (session.connection === connection) {
return session;
}
}
console.info('Session not found for a connection');
return undefined;
}
/**
* FIXME.
*
* @param {Object | Function} stateful - FIXME.
* @param {JitsiConference} conference - FIXME.
* @returns {Object|undefined}
*/
function findSessionForConference(stateful, conference) {
const state = toState(stateful);
for (const session of state['features/base/session'].values()) {
if (session.conference === conference) {
return session;
}
}
console.info('Session not found for a conference');
return undefined;
}

View File

@ -0,0 +1,71 @@
// @flow
import { assign, ReducerRegistry } from '../../base/redux';
import { getSymbolDescription } from '../util';
import { SET_SESSION } from './actionTypes';
import {
SESSION_FAILED,
SESSION_ENDED,
SESSION_WILL_START
} from './constants';
ReducerRegistry.register('features/base/session',
(state = new Map(), action) => {
switch (action.type) {
case SET_SESSION:
return _setSession(state, action);
}
return state;
});
/**
* FIXME.
*
* @param {Object} featureState - FIXME.
* @param {Object} action - FIXME.
* @returns {Map<any, any>} - FIXME.
* @private
*/
function _setSession(featureState, action) {
const { url, state, ...data } = action.session;
const session = featureState.get(url);
const nextState = new Map(featureState);
// Drop the whole action if the url is not defined
if (!url) {
console.error('SET SESSION - NO URL');
return nextState;
}
if (session) {
if (state === SESSION_ENDED || state === SESSION_FAILED) {
nextState.delete(url);
} else {
nextState.set(
url,
assign(session, {
url,
state: state ? state : session.state,
...data
}));
}
} else if (state === SESSION_WILL_START) {
nextState.set(
url, {
url,
state,
...data
});
}
console.info(
'SESSION STATE REDUCED: ',
new Map(nextState),
url,
state && getSymbolDescription(state),
action.session.error);
return nextState;
}

View File

@ -5,14 +5,7 @@ import uuid from 'uuid';
import { createTrackMutedEvent, sendAnalytics } from '../../analytics';
import { appNavigate, getName } from '../../app';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
import {
CONFERENCE_FAILED,
CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN,
CONFERENCE_JOINED,
SET_AUDIO_ONLY,
getCurrentConference
} from '../../base/conference';
import { SET_AUDIO_ONLY } from '../../base/conference';
import { getInviteURL } from '../../base/connection';
import {
MEDIA_TYPE,
@ -20,6 +13,16 @@ import {
setAudioMuted
} from '../../base/media';
import { MiddlewareRegistry } from '../../base/redux';
import {
SESSION_CONFIGURED,
SESSION_ENDED,
SESSION_FAILED,
SESSION_STARTED,
SET_SESSION,
getCurrentSession,
getSession,
setSession
} from '../../base/session';
import {
TRACK_ADDED,
TRACK_REMOVED,
@ -51,21 +54,12 @@ CallKit && MiddlewareRegistry.register(store => next => action => {
});
break;
case CONFERENCE_FAILED:
return _conferenceFailed(store, next, action);
case CONFERENCE_JOINED:
return _conferenceJoined(store, next, action);
case CONFERENCE_LEFT:
return _conferenceLeft(store, next, action);
case CONFERENCE_WILL_JOIN:
return _conferenceWillJoin(store, next, action);
case SET_AUDIO_ONLY:
return _setAudioOnly(store, next, action);
case SET_SESSION:
return _setSession(store, next, action);
case TRACK_ADDED:
case TRACK_REMOVED:
case TRACK_UPDATED:
@ -127,6 +121,35 @@ function _appWillMount({ dispatch, getState }, next, action) {
return result;
}
/**
* FIXME.
*
* @param {Store}store - FIXME.
* @param {Dispatch} next - FIXME.
* @param {Action} action - FIXME.
* @returns {*} The value returned by {@code next(action)}.
* @private
*/
function _setSession(store, next, action) {
const { state } = action.session;
switch (state) {
case SESSION_CONFIGURED:
return _sessionConfigured(store, next, action);
case SESSION_ENDED:
return _sessionEnded(store, next, action);
case SESSION_FAILED:
return _sessionFailed(store, next, action);
case SESSION_STARTED:
return _sessionJoined(store, next, action);
}
return next(action);
}
/**
* Notifies the feature callkit that the action {@link CONFERENCE_FAILED} is
* being dispatched within a specific redux {@code store}.
@ -140,21 +163,14 @@ function _appWillMount({ dispatch, getState }, next, action) {
* @private
* @returns {*} The value returned by {@code next(action)}.
*/
function _conferenceFailed(store, next, action) {
const result = next(action);
function _sessionFailed(store, next, action) {
const callUUID = _getCallUUIDForSessionAction(store, action);
// XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
// prevented the user from joining a specific conference but the app may be
// able to eventually join the conference.
if (!action.error.recoverable) {
const { callUUID } = action.conference;
if (callUUID) {
CallKit.reportCallFailed(callUUID);
}
if (callUUID) {
CallKit.reportCallFailed(callUUID);
}
return result;
return next(action);
}
/**
@ -170,16 +186,14 @@ function _conferenceFailed(store, next, action) {
* @private
* @returns {*} The value returned by {@code next(action)}.
*/
function _conferenceJoined(store, next, action) {
const result = next(action);
const { callUUID } = action.conference;
function _sessionJoined(store, next, action) {
const callUUID = _getCallUUIDForSessionAction(store, action);
if (callUUID) {
CallKit.reportConnectedOutgoingCall(callUUID);
}
return result;
return next(action);
}
/**
@ -195,16 +209,34 @@ function _conferenceJoined(store, next, action) {
* @private
* @returns {*} The value returned by {@code next(action)}.
*/
function _conferenceLeft(store, next, action) {
const result = next(action);
const { callUUID } = action.conference;
function _sessionEnded(store, next, action) {
const callUUID = _getCallUUIDForSessionAction(store, action);
if (callUUID) {
CallKit.endCall(callUUID);
}
return result;
return next(action);
}
/**
* FIXME.
*
* @param {Store} store - FIXME.
* @param {Object} action - FIXME.
* @returns {string|undefined}
* @private
*/
function _getCallUUIDForSessionAction(store, action) {
const url = action.session.url;
const session = getSession(store, url);
const callUUID = session && session.callkit && session.callkit.callUUID;
if (!callUUID) {
console.info(`CALLKIT SESSION NOT FOUND FOR URL: ${url}`);
}
return callUUID;
}
/**
@ -220,27 +252,32 @@ function _conferenceLeft(store, next, action) {
* @private
* @returns {*} The value returned by {@code next(action)}.
*/
function _conferenceWillJoin({ getState }, next, action) {
const result = next(action);
const { conference } = action;
function _sessionConfigured({ getState }, next, action) {
const state = getState();
const { callHandle, callUUID } = state['features/base/config'];
const { callHandle, callUUID: _callUUID } = state['features/base/config'];
const url = getInviteURL(state);
const handle = callHandle || url.toString();
const hasVideo = !isVideoMutedByAudioOnly(state);
// When assigning the call UUID, do so in upper case, since iOS will return
// it upper cased.
conference.callUUID = (callUUID || uuid.v4()).toUpperCase();
const callUUID = (_callUUID || uuid.v4()).toUpperCase();
CallKit.startCall(conference.callUUID, handle, hasVideo)
// Store the callUUID in the session
action.session.callkit = {
callUUID
};
CallKit.startCall(callUUID, handle, hasVideo)
.then(() => {
const session = getSession(getState(), action.session.url);
const { callee } = state['features/base/jwt'];
const displayName
= state['features/base/config'].callDisplayName
|| (callee && callee.name)
|| state['features/base/conference'].room;
|| (session && session.room);
console.info(`CALLKIT WILL USE NAME: ${displayName}`);
const muted
= isLocalTrackMuted(
@ -248,11 +285,11 @@ function _conferenceWillJoin({ getState }, next, action) {
MEDIA_TYPE.AUDIO);
// eslint-disable-next-line object-property-newline
CallKit.updateCall(conference.callUUID, { displayName, hasVideo });
CallKit.setMuted(conference.callUUID, muted);
CallKit.updateCall(callUUID, { displayName, hasVideo });
CallKit.setMuted(callUUID, muted);
});
return result;
return next(action);
}
/**
@ -263,18 +300,47 @@ function _conferenceWillJoin({ getState }, next, action) {
* @returns {void}
*/
function _onPerformEndCallAction({ callUUID }) {
const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
const conference = getCurrentConference(getState);
const { dispatch } = this; // eslint-disable-line no-invalid-this
// eslint-disable-next-line max-len
const session = _findSessionForCallUUID(this, callUUID); // eslint-disable-line no-invalid-this
if (conference && conference.callUUID === callUUID) {
if (session) {
// We arrive here when a call is ended by the system, for example, when
// another incoming call is received and the user selects "End &
// Accept".
delete conference.callUUID;
dispatch(
setSession({
url: session.url,
callkit: undefined
}));
dispatch(appNavigate(undefined));
}
}
/**
* FIXME.
*
* @param {Store} getState - FIXME.
* @param {string} callUUID - FIXME.
* @returns {Object|null}
* @private
*/
function _findSessionForCallUUID({ getState }, callUUID) {
const sessions = getState()['features/base/session'];
for (const session of sessions.values()) {
const _callUUID = session.callkit && session.callkit.callUUID;
if (callUUID === _callUUID) {
return session;
}
}
console.info(`SESSION NOT FOUND FOR CALL ID: ${callUUID}`);
return null;
}
/**
* Handles CallKit's event {@code performSetMutedCallAction}.
*
@ -283,10 +349,11 @@ function _onPerformEndCallAction({ callUUID }) {
* @returns {void}
*/
function _onPerformSetMutedCallAction({ callUUID, muted }) {
const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
const conference = getCurrentConference(getState);
const { dispatch } = this; // eslint-disable-line no-invalid-this
// eslint-disable-next-line max-len
const session = _findSessionForCallUUID(this, callUUID); // eslint-disable-line no-invalid-this
if (conference && conference.callUUID === callUUID) {
if (session) {
muted = Boolean(muted); // eslint-disable-line no-param-reassign
sendAnalytics(createTrackMutedEvent('audio', 'callkit', muted));
dispatch(setAudioMuted(muted, /* ensureTrack */ true));
@ -316,11 +383,11 @@ function _onPerformSetMutedCallAction({ callUUID, muted }) {
function _setAudioOnly({ getState }, next, action) {
const result = next(action);
const state = getState();
const conference = getCurrentConference(state);
const session = getCurrentSession(state);
if (conference && conference.callUUID) {
if (session && session.callUUID) {
CallKit.updateCall(
conference.callUUID,
session.callUUID,
{ hasVideo: !action.audioOnly });
}
@ -369,20 +436,25 @@ function _syncTrackState({ getState }, next, action) {
const result = next(action);
const { jitsiTrack } = action.track;
const state = getState();
const conference = getCurrentConference(state);
if (jitsiTrack.isLocal() && conference && conference.callUUID) {
// It could go over all sessions here, but even if we'd support simultaneous
// sessions / putting on hold, probably only the active session would be
// holding the tracks.
const session = getCurrentSession(state);
const callUUID = session && session.callkit && session.callkit.callUUID;
if (jitsiTrack.isLocal() && callUUID) {
switch (jitsiTrack.getType()) {
case 'audio': {
const tracks = state['features/base/tracks'];
const muted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
CallKit.setMuted(conference.callUUID, muted);
CallKit.setMuted(callUUID, muted);
break;
}
case 'video': {
CallKit.updateCall(
conference.callUUID,
callUUID,
{ hasVideo: !isVideoMutedByAudioOnly(state) });
break;
}

View File

@ -1,22 +1,56 @@
// @flow
import { NativeModules } from 'react-native';
import { getAppProp } from '../../app';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN,
CONFERENCE_WILL_LEAVE,
JITSI_CONFERENCE_URL_KEY,
SET_ROOM,
forEachConference,
isRoomValid
CONFERENCE_WILL_LEAVE
} from '../../base/conference';
import { LOAD_CONFIG_ERROR } from '../../base/config';
import { CONNECTION_FAILED } from '../../base/connection';
import { MiddlewareRegistry } from '../../base/redux';
import { getSymbolDescription, toURLString } from '../../base/util';
import {
SESSION_ENDED,
SESSION_FAILED,
SESSION_STARTED,
SESSION_WILL_END,
SESSION_WILL_START,
SET_SESSION
} from '../../base/session';
import { getSymbolDescription } from '../../base/util';
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture';
/**
* FIXME.
*
* @param {Symbol} state - FIXME.
* @returns {string}
* @private
*/
function _stateToApiEventName(state) {
switch (state) {
case SESSION_WILL_START:
return getSymbolDescription(CONFERENCE_WILL_JOIN);
case SESSION_STARTED:
return getSymbolDescription(CONFERENCE_JOINED);
case SESSION_WILL_END:
return getSymbolDescription(CONFERENCE_WILL_LEAVE);
case SESSION_ENDED:
return getSymbolDescription(CONFERENCE_LEFT);
case SESSION_FAILED:
return getSymbolDescription(CONFERENCE_FAILED);
default:
return undefined;
}
}
import { sendEvent } from './functions';
/**
@ -27,63 +61,19 @@ import { sendEvent } from './functions';
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
action.type && console.info(`ACTION ${getSymbolDescription(action.type)}`);
const result = next(action);
const { type } = action;
switch (type) {
case CONFERENCE_FAILED: {
const { error, ...data } = action;
// XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
// prevented the user from joining a specific conference but the app may
// be able to eventually join the conference. For example, the app will
// ask the user for a password upon
// JitsiConferenceErrors.PASSWORD_REQUIRED and will retry joining the
// conference afterwards. Such errors are to not reach the native
// counterpart of the External API (or at least not in the
// fatality/finality semantics attributed to
// conferenceFailed:/onConferenceFailed).
if (!error.recoverable) {
_sendConferenceEvent(store, /* action */ {
error: _toErrorString(error),
...data
});
}
break;
}
case CONFERENCE_JOINED:
case CONFERENCE_LEFT:
case CONFERENCE_WILL_JOIN:
case CONFERENCE_WILL_LEAVE:
_sendConferenceEvent(store, action);
break;
case CONNECTION_FAILED:
!action.error.recoverable
&& _sendConferenceFailedOnConnectionError(store, action);
case SET_SESSION:
_setSession(store, action);
break;
case ENTER_PICTURE_IN_PICTURE:
sendEvent(store, getSymbolDescription(type), /* data */ {});
break;
case LOAD_CONFIG_ERROR: {
const { error, locationURL } = action;
sendEvent(
store,
getSymbolDescription(type),
/* data */ {
error: _toErrorString(error),
url: toURLString(locationURL)
});
break;
}
case SET_ROOM:
_maybeTriggerEarlyConferenceWillJoin(store, action);
break;
}
return result;
@ -110,145 +100,47 @@ function _toErrorString(
}
/**
* If {@link SET_ROOM} action happens for a valid conference room this method
* will emit an early {@link CONFERENCE_WILL_JOIN} event to let the external API
* know that a conference is being joined. Before that happens a connection must
* be created and only then base/conference feature would emit
* {@link CONFERENCE_WILL_JOIN}. That is fine for the Jitsi Meet app, because
* that's the a conference instance gets created, but it's too late for
* the external API to learn that. The latter {@link CONFERENCE_WILL_JOIN} is
* swallowed in {@link _swallowEvent}.
*
* @param {Store} store - The redux store.
* @param {Action} action - The redux action.
* @returns {void}
*/
function _maybeTriggerEarlyConferenceWillJoin(store, action) {
const { locationURL } = store.getState()['features/base/connection'];
const { room } = action;
isRoomValid(room) && locationURL && sendEvent(
store,
getSymbolDescription(CONFERENCE_WILL_JOIN),
/* data */ {
url: toURLString(locationURL)
});
}
/**
* Sends an event to the native counterpart of the External API for a specific
* conference-related redux action.
*
* @param {Store} store - The redux store.
* @param {Action} action - The redux action.
* @returns {void}
*/
function _sendConferenceEvent(
store: Object,
action: {
conference: Object,
type: Symbol,
url: ?string
}) {
const { conference, type, ...data } = action;
// For these (redux) actions, conference identifies a JitsiConference
// instance. The external API cannot transport such an object so we have to
// transport an "equivalent".
if (conference) {
data.url = toURLString(conference[JITSI_CONFERENCE_URL_KEY]);
}
_swallowEvent(store, action, data)
|| sendEvent(store, getSymbolDescription(type), data);
}
/**
* Sends {@link CONFERENCE_FAILED} event when the {@link CONNECTION_FAILED}
* occurs. It should be done only if the connection fails before the conference
* instance is created. Otherwise the eventual failure event is supposed to be
* emitted by the base/conference feature.
*
* @param {Store} store - The redux store.
* @param {Action} action - The redux action.
* @returns {void}
*/
function _sendConferenceFailedOnConnectionError(store, action) {
const { locationURL } = store.getState()['features/base/connection'];
const { connection } = action;
locationURL
&& forEachConference(
store,
// If there's any conference in the base/conference state then the
// base/conference feature is supposed to emit a failure.
conference => conference.getConnection() !== connection)
&& sendEvent(
store,
getSymbolDescription(CONFERENCE_FAILED),
/* data */ {
url: toURLString(locationURL),
error: action.error.name
});
}
/**
* Determines whether to not send a {@code CONFERENCE_LEFT} event to the native
* counterpart of the External API.
* Sends a specific event to the native counterpart of the External API. Native
* apps may listen to such events via the mechanisms provided by the (native)
* mobile Jitsi Meet SDK.
*
* @param {Object} store - The redux store.
* @param {Action} action - The redux action which is causing the sending of the
* event.
* @param {string} name - The name of the event to send.
* @param {Object} data - The details/specifics of the event to send determined
* by/associated with the specified {@code action}.
* @returns {boolean} If the specified event is to not be sent, {@code true};
* otherwise, {@code false}.
* by/associated with the specified {@code name}.
* @private
* @returns {void}
*/
function _swallowConferenceLeft({ getState }, action, { url }) {
// XXX Internally, we work with JitsiConference instances. Externally
// though, we deal with URL strings. The relation between the two is many to
// one so it's technically and practically possible (by externally loading
// the same URL string multiple times) to try to send CONFERENCE_LEFT
// externally for a URL string which identifies a JitsiConference that the
// app is internally legitimately working with.
let swallowConferenceLeft = false;
function _sendEvent(store: Object, name: string, data: Object) {
// The JavaScript App needs to provide uniquely identifying information to
// the native ExternalAPI module so that the latter may match the former to
// the native JitsiMeetView which hosts it.
const externalAPIScope = getAppProp(store, 'externalAPIScope');
url
&& forEachConference(getState, (conference, conferenceURL) => {
if (conferenceURL && conferenceURL.toString() === url) {
swallowConferenceLeft = true;
}
console.info(
`EXT EVENT ${name} URL: ${data.url} DATA: ${JSON.stringify(data)}`);
return !swallowConferenceLeft;
});
return swallowConferenceLeft;
externalAPIScope
&& NativeModules.ExternalAPI.sendEvent(name, data, externalAPIScope);
}
/**
* Determines whether to not send a specific event to the native counterpart of
* the External API.
* FIXME.
*
* @param {Object} store - The redux store.
* @param {Action} action - The redux action which is causing the sending of the
* event.
* @param {Object} data - The details/specifics of the event to send determined
* by/associated with the specified {@code action}.
* @returns {boolean} If the specified event is to not be sent, {@code true};
* otherwise, {@code false}.
* @param {Store} store - FIXME.
* @param {Action} action - FIXME.
* @returns {void}
* @private
*/
function _swallowEvent(store, action, data) {
switch (action.type) {
case CONFERENCE_LEFT:
return _swallowConferenceLeft(store, action, data);
case CONFERENCE_WILL_JOIN:
// CONFERENCE_WILL_JOIN is dispatched to the external API on SET_ROOM,
// before the connection is created, so we need to swallow the original
// one emitted by base/conference.
return true;
function _setSession(store, action) {
const { error, state, url } = action.session;
const apiEventName = _stateToApiEventName(state);
default:
return false;
}
apiEventName && _sendEvent(
store,
apiEventName,
/* data */ {
url,
error: error && _toErrorString(error)
});
}

View File

@ -1,3 +1,9 @@
import {
SESSION_FAILED,
getCurrentSession,
setSession
} from '../base/session';
import {
MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
SET_FATAL_ERROR,
@ -57,3 +63,37 @@ export function setFatalError(fatalError) {
fatalError
};
}
/**
* FIXME naming is not quite accurate - came from the previous method which was
* reemitting the action. I feel that this part needs more discussion. Changing
* it back to emitting the original action which caused the fatal error will
* also require changes to how it's being detected (currently through the state
* listener, but we'd have to go back to the middleware way).
*
* @returns {Function}
*/
export function reemitFatalError() {
return (dispatch, getState) => {
const state = getState();
const { fatalError } = state['features/overlay'];
if (fatalError) {
const session = getCurrentSession(state);
if (session) {
dispatch(
setSession({
url: session.url,
state: SESSION_FAILED,
error: fatalError
}));
} else {
console.info('No current session!');
}
dispatch(setFatalError(undefined));
} else {
console.info('NO FATAL ERROR');
}
};
}

View File

@ -8,7 +8,7 @@ import { LoadingIndicator } from '../../base/react';
import AbstractPageReloadOverlay, { abstractMapStateToProps }
from './AbstractPageReloadOverlay';
import { setFatalError } from '../actions';
import { reemitFatalError } from '../actions';
import OverlayFrame from './OverlayFrame';
import { pageReloadOverlay as styles } from './styles';
@ -41,7 +41,7 @@ class PageReloadOverlay extends AbstractPageReloadOverlay {
*/
_onCancel() {
clearInterval(this._interval);
this.props.dispatch(setFatalError(undefined));
this.props.dispatch(reemitFatalError());
this.props.dispatch(appNavigate(undefined));
}