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,84 @@
/**
* The type of (redux) action which signals that a connection disconnected.
*
* {
* type: CONNECTION_DISCONNECTED,
* connection: JitsiConnection
* }
*/
export const CONNECTION_DISCONNECTED = 'CONNECTION_DISCONNECTED';
/**
* The type of (redux) action which signals that a connection was successfully
* established.
*
* {
* type: CONNECTION_ESTABLISHED,
* connection: JitsiConnection,
* timeEstablished: number,
* }
*/
export const CONNECTION_ESTABLISHED = 'CONNECTION_ESTABLISHED';
/**
* The type of (redux) action which signals that a connection failed.
*
* {
* type: CONNECTION_FAILED,
* connection: JitsiConnection,
* error: Object | string
* }
*/
export const CONNECTION_FAILED = 'CONNECTION_FAILED';
/**
* The type of (redux) action which signals that connection properties were updated.
*
* {
* type: CONNECTION_PROPERTIES_UPDATED,
* properties: Object
* }
*/
export const CONNECTION_PROPERTIES_UPDATED = 'CONNECTION_PROPERTIES_UPDATED';
/**
* The type of (redux) action which signals that a connection will connect.
*
* {
* type: CONNECTION_WILL_CONNECT,
* connection: JitsiConnection
* }
*/
export const CONNECTION_WILL_CONNECT = 'CONNECTION_WILL_CONNECT';
/**
* The type of (redux) action which sets the location URL of the application,
* connection, conference, etc.
*
* {
* type: SET_LOCATION_URL,
* locationURL: ?URL
* }
*/
export const SET_LOCATION_URL = 'SET_LOCATION_URL';
/**
* The type of (redux) action which sets the preferVisitor in store.
*
* {
* type: SET_PREFER_VISITOR,
* preferVisitor: ?boolean
* }
*/
export const SET_PREFER_VISITOR = 'SET_PREFER_VISITOR';
/**
* The type of (redux) action which tells whether connection info should be displayed
* on context menu.
*
* {
* type: SHOW_CONNECTION_INFO,
* showConnectionInfo: boolean
* }
*/
export const SHOW_CONNECTION_INFO = 'SHOW_CONNECTION_INFO';

View File

@@ -0,0 +1,442 @@
import { cloneDeep } from 'lodash-es';
import { IReduxState, IStore } from '../../app/types';
import { conferenceLeft, conferenceWillLeave, redirect } from '../conference/actions';
import { getCurrentConference } from '../conference/functions';
import { IConfigState } from '../config/reducer';
import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
import { isEmbedded } from '../util/embedUtils';
import { parseURLParams } from '../util/parseURLParams';
import {
appendURLParam,
getBackendSafeRoomName
} from '../util/uri';
import {
CONNECTION_DISCONNECTED,
CONNECTION_ESTABLISHED,
CONNECTION_FAILED,
CONNECTION_PROPERTIES_UPDATED,
CONNECTION_WILL_CONNECT,
SET_LOCATION_URL,
SET_PREFER_VISITOR
} from './actionTypes';
import { JITSI_CONNECTION_URL_KEY } from './constants';
import logger from './logger';
import { ConnectionFailedError, IIceServers } from './types';
/**
* The options that will be passed to the JitsiConnection instance.
*/
interface IOptions extends IConfigState {
iceServersOverride?: IIceServers;
preferVisitor?: boolean;
}
/**
* Create an action for when the signaling connection has been lost.
*
* @param {JitsiConnection} connection - The {@code JitsiConnection} which
* disconnected.
* @private
* @returns {{
* type: CONNECTION_DISCONNECTED,
* connection: JitsiConnection
* }}
*/
export function connectionDisconnected(connection?: Object) {
return {
type: CONNECTION_DISCONNECTED,
connection
};
}
/**
* Create an action for when the signaling connection has been established.
*
* @param {JitsiConnection} connection - The {@code JitsiConnection} which was
* established.
* @param {number} timeEstablished - The time at which the
* {@code JitsiConnection} which was established.
* @public
* @returns {{
* type: CONNECTION_ESTABLISHED,
* connection: JitsiConnection,
* timeEstablished: number
* }}
*/
export function connectionEstablished(
connection: Object, timeEstablished: number) {
return {
type: CONNECTION_ESTABLISHED,
connection,
timeEstablished
};
}
/**
* Create an action for when the signaling connection could not be created.
*
* @param {JitsiConnection} connection - The {@code JitsiConnection} which
* failed.
* @param {ConnectionFailedError} error - Error.
* @public
* @returns {{
* type: CONNECTION_FAILED,
* connection: JitsiConnection,
* error: ConnectionFailedError
* }}
*/
export function connectionFailed(
connection: Object,
error: ConnectionFailedError) {
const { credentials } = error;
if (credentials && !Object.keys(credentials).length) {
error.credentials = undefined;
}
return {
type: CONNECTION_FAILED,
connection,
error
};
}
/**
* Constructs options to be passed to the constructor of {@code JitsiConnection}
* based on the redux state.
*
* @param {Object} state - The redux state.
* @returns {Object} The options to be passed to the constructor of
* {@code JitsiConnection}.
*/
export function constructOptions(state: IReduxState) {
// Deep clone the options to make sure we don't modify the object in the
// redux store.
const options: IOptions = cloneDeep(state['features/base/config']);
const { locationURL, preferVisitor } = state['features/base/connection'];
const params = parseURLParams(locationURL || '');
const iceServersOverride = params['iceServers.replace'];
// Allow iceServersOverride only when jitsi-meet is in an iframe.
if (isEmbedded() && iceServersOverride) {
options.iceServersOverride = iceServersOverride;
}
const { bosh, preferBosh, flags } = options;
let { websocket } = options;
if (preferBosh) {
websocket = undefined;
}
// WebSocket is preferred over BOSH.
const serviceUrl = websocket || bosh;
logger.log(`Using service URL ${serviceUrl}`);
// Append room to the URL's search.
const { room } = state['features/base/conference'];
if (serviceUrl && room) {
const roomName = getBackendSafeRoomName(room);
options.serviceUrl = appendURLParam(serviceUrl, 'room', roomName ?? '');
if (options.websocketKeepAliveUrl) {
options.websocketKeepAliveUrl = appendURLParam(options.websocketKeepAliveUrl, 'room', roomName ?? '');
}
if (options.conferenceRequestUrl) {
options.conferenceRequestUrl = appendURLParam(options.conferenceRequestUrl, 'room', roomName ?? '');
}
}
if (preferVisitor) {
options.preferVisitor = true;
}
// Enable ssrc-rewriting by default.
if (typeof flags?.ssrcRewritingEnabled === 'undefined') {
const { ...otherFlags } = flags ?? {};
options.flags = {
...otherFlags,
ssrcRewritingEnabled: true
};
}
return options;
}
/**
* Sets the location URL of the application, connection, conference, etc.
*
* @param {URL} [locationURL] - The location URL of the application,
* connection, conference, etc.
* @returns {{
* type: SET_LOCATION_URL,
* locationURL: URL
* }}
*/
export function setLocationURL(locationURL?: URL) {
return {
type: SET_LOCATION_URL,
locationURL
};
}
/**
* To change prefer visitor in the store. Used later to decide what to request from jicofo on connection.
*
* @param {boolean} preferVisitor - The value to set.
* @returns {{
* type: SET_PREFER_VISITOR,
* preferVisitor: boolean
* }}
*/
export function setPreferVisitor(preferVisitor: boolean) {
return {
type: SET_PREFER_VISITOR,
preferVisitor
};
}
/**
* Opens new connection.
*
* @param {string} [id] - The XMPP user's ID (e.g. {@code user@server.com}).
* @param {string} [password] - The XMPP user's password.
* @returns {Function}
*/
export function _connectInternal(id?: string, password?: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const options = constructOptions(state);
const { locationURL } = state['features/base/connection'];
const { jwt } = state['features/base/jwt'];
const connection = new JitsiMeetJS.JitsiConnection(options.appId, jwt, options);
connection[JITSI_CONNECTION_URL_KEY] = locationURL;
dispatch(_connectionWillConnect(connection));
return new Promise((resolve, reject) => {
connection.addEventListener(
JitsiConnectionEvents.CONNECTION_DISCONNECTED,
_onConnectionDisconnected);
connection.addEventListener(
JitsiConnectionEvents.CONNECTION_ESTABLISHED,
_onConnectionEstablished);
connection.addEventListener(
JitsiConnectionEvents.CONNECTION_FAILED,
_onConnectionFailed);
connection.addEventListener(
JitsiConnectionEvents.CONNECTION_REDIRECTED,
_onConnectionRedirected);
connection.addEventListener(
JitsiConnectionEvents.PROPERTIES_UPDATED,
_onPropertiesUpdate);
/**
* Unsubscribe the connection instance from
* {@code CONNECTION_DISCONNECTED} and {@code CONNECTION_FAILED} events.
*
* @returns {void}
*/
function unsubscribe() {
connection.removeEventListener(
JitsiConnectionEvents.CONNECTION_DISCONNECTED, _onConnectionDisconnected);
connection.removeEventListener(JitsiConnectionEvents.CONNECTION_FAILED, _onConnectionFailed);
connection.removeEventListener(JitsiConnectionEvents.PROPERTIES_UPDATED, _onPropertiesUpdate);
}
/**
* Dispatches {@code CONNECTION_DISCONNECTED} action when connection is
* disconnected.
*
* @private
* @returns {void}
*/
function _onConnectionDisconnected() {
unsubscribe();
dispatch(connectionDisconnected(connection));
resolve(connection);
}
/**
* Rejects external promise when connection fails.
*
* @param {JitsiConnectionErrors} err - Connection error.
* @param {string} [message] - Error message supplied by lib-jitsi-meet.
* @param {Object} [credentials] - The invalid credentials that were
* used to authenticate and the authentication failed.
* @param {string} [credentials.jid] - The XMPP user's ID.
* @param {string} [credentials.password] - The XMPP user's password.
* @param {Object} details - Additional information about the error.
* @private
* @returns {void}
*/
function _onConnectionFailed( // eslint-disable-line max-params
err: string,
message: string,
credentials: any,
details: Object) {
unsubscribe();
dispatch(connectionFailed(connection, {
credentials,
details,
name: err,
message
}));
reject(err);
}
/**
* Resolves external promise when connection is established.
*
* @private
* @returns {void}
*/
function _onConnectionEstablished() {
connection.removeEventListener(JitsiConnectionEvents.CONNECTION_ESTABLISHED, _onConnectionEstablished);
dispatch(connectionEstablished(connection, Date.now()));
resolve(connection);
}
/**
* Connection was redirected.
*
* @param {string|undefined} vnode - The vnode to connect to.
* @param {string} focusJid - The focus jid to use.
* @param {string|undefined} username - The username to use when joining. This is after promotion from
* visitor to main participant.
* @private
* @returns {void}
*/
function _onConnectionRedirected(vnode: string, focusJid: string, username: string) {
connection.removeEventListener(JitsiConnectionEvents.CONNECTION_REDIRECTED, _onConnectionRedirected);
dispatch(redirect(vnode, focusJid, username));
}
/**
* Connection properties were updated.
*
* @param {Object} properties - The properties which were updated.
* @private
* @returns {void}
*/
function _onPropertiesUpdate(properties: object) {
dispatch(_propertiesUpdate(properties));
}
// in case of configured http url for conference request we need the room name
const name = getBackendSafeRoomName(state['features/base/conference'].room);
connection.connect({
id,
password,
name
});
});
};
}
/**
* Create an action for when a connection will connect.
*
* @param {JitsiConnection} connection - The {@code JitsiConnection} which will
* connect.
* @private
* @returns {{
* type: CONNECTION_WILL_CONNECT,
* connection: JitsiConnection
* }}
*/
function _connectionWillConnect(connection: Object) {
return {
type: CONNECTION_WILL_CONNECT,
connection
};
}
/**
* Create an action for when connection properties are updated.
*
* @param {Object} properties - The properties which were updated.
* @private
* @returns {{
* type: CONNECTION_PROPERTIES_UPDATED,
* properties: Object
* }}
*/
function _propertiesUpdate(properties: object) {
return {
type: CONNECTION_PROPERTIES_UPDATED,
properties
};
}
/**
* Closes connection.
*
* @param {boolean} isRedirect - Indicates if the action has been dispatched as part of visitor promotion.
*
* @returns {Function}
*/
export function disconnect(isRedirect?: boolean) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']): Promise<void> => {
const state = getState();
// The conference we have already joined or are joining.
const conference_ = getCurrentConference(state);
// Promise which completes when the conference has been left and the
// connection has been disconnected.
let promise;
// Leave the conference.
if (conference_) {
// In a fashion similar to JitsiConference's CONFERENCE_LEFT event
// (and the respective Redux action) which is fired after the
// conference has been left, notify the application about the
// intention to leave the conference.
dispatch(conferenceWillLeave(conference_, isRedirect));
promise
= conference_.leave()
.catch((error: Error) => {
logger.warn(
'JitsiConference.leave() rejected with:',
error);
// The library lib-jitsi-meet failed to make the
// JitsiConference leave. Which may be because
// JitsiConference thinks it has already left.
// Regardless of the failure reason, continue in
// jitsi-meet as if the leave has succeeded.
dispatch(conferenceLeft(conference_));
});
} else {
promise = Promise.resolve();
}
// Disconnect the connection.
const { connecting, connection } = state['features/base/connection'];
// The connection we have already connected or are connecting.
const connection_ = connection || connecting;
if (connection_) {
promise = promise.then(() => connection_.disconnect());
} else {
logger.info('No connection found while disconnecting.');
}
return promise;
};
}

View File

@@ -0,0 +1,62 @@
import { appNavigate } from '../../app/actions.native';
import { IStore } from '../../app/types';
import { getCustomerDetails } from '../../jaas/actions.any';
import { getJaasJWT, isVpaasMeeting } from '../../jaas/functions';
import { navigateRoot } from '../../mobile/navigation/rootNavigationContainerRef';
import { screen } from '../../mobile/navigation/routes';
import { setJWT } from '../jwt/actions';
import { JitsiConnectionErrors } from '../lib-jitsi-meet';
import { _connectInternal } from './actions.native';
import logger from './logger';
export * from './actions.any';
/**
* Opens new connection.
*
* @param {string} [id] - The XMPP user's ID (e.g. {@code user@server.com}).
* @param {string} [password] - The XMPP user's password.
* @returns {Function}
*/
export function connect(id?: string, password?: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const { jwt } = state['features/base/jwt'];
if (isVpaasMeeting(state)) {
return dispatch(getCustomerDetails())
.then(() => {
if (!jwt) {
return getJaasJWT(state);
}
})
.then(j => {
j && dispatch(setJWT(j));
return dispatch(_connectInternal(id, password));
}).catch(e => {
logger.error('Connection error', e);
});
}
dispatch(_connectInternal(id, password))
.catch(error => {
if (error === JitsiConnectionErrors.NOT_LIVE_ERROR) {
navigateRoot(screen.visitorsQueue);
}
});
};
}
/**
* Hangup.
*
* @param {boolean} [_requestFeedback] - Whether to attempt showing a
* request for call feedback.
* @returns {Function}
*/
export function hangup(_requestFeedback = false) {
return (dispatch: IStore['dispatch']) => dispatch(appNavigate(undefined));
}

View File

@@ -0,0 +1,90 @@
// @ts-expect-error
import { jitsiLocalStorage } from '@jitsi/js-utils';
import { IStore } from '../../app/types';
import { getCustomerDetails } from '../../jaas/actions.any';
import { getJaasJWT, isVpaasMeeting } from '../../jaas/functions';
import { showWarningNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import { stopLocalVideoRecording } from '../../recording/actions.any';
import LocalRecordingManager from '../../recording/components/Recording/LocalRecordingManager.web';
import { setJWT } from '../jwt/actions';
import { _connectInternal } from './actions.any';
import logger from './logger';
export * from './actions.any';
/**
* Opens new connection.
*
* @param {string} [id] - The XMPP user's ID (e.g. {@code user@server.com}).
* @param {string} [password] - The XMPP user's password.
* @returns {Function}
*/
export function connect(id?: string, password?: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const { jwt } = state['features/base/jwt'];
const { iAmRecorder, iAmSipGateway } = state['features/base/config'];
if (!iAmRecorder && !iAmSipGateway && isVpaasMeeting(state)) {
return dispatch(getCustomerDetails())
.then(() => {
if (!jwt) {
return getJaasJWT(state);
}
})
.then(j => {
j && dispatch(setJWT(j));
return dispatch(_connectInternal(id, password));
}).catch(e => {
logger.error('Connection error', e);
});
}
// used by jibri
const usernameOverride = jitsiLocalStorage.getItem('xmpp_username_override');
const passwordOverride = jitsiLocalStorage.getItem('xmpp_password_override');
if (usernameOverride && usernameOverride.length > 0) {
id = usernameOverride; // eslint-disable-line no-param-reassign
}
if (passwordOverride && passwordOverride.length > 0) {
password = passwordOverride; // eslint-disable-line no-param-reassign
}
return dispatch(_connectInternal(id, password));
};
}
/**
* Closes connection.
*
* @param {boolean} [requestFeedback] - Whether to attempt showing a
* request for call feedback.
* @param {string} [feedbackTitle] - The feedback title.
* @param {boolean} [notifyOnConferenceTermination] - Whether to notify
* the user on conference termination.
* @returns {Function}
*/
export function hangup(requestFeedback = false, feedbackTitle?: string, notifyOnConferenceTermination?: boolean) {
// XXX For web based version we use conference hanging up logic from the old app.
return async (dispatch: IStore['dispatch']) => {
if (LocalRecordingManager.isRecordingLocally()) {
dispatch(stopLocalVideoRecording());
dispatch(showWarningNotification({
titleKey: 'localRecording.stopping',
descriptionKey: 'localRecording.wait'
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
// wait 1000ms for the recording to end and start downloading
await new Promise(res => {
setTimeout(res, 1000);
});
}
return APP.conference.hangup(requestFeedback, feedbackTitle, notifyOnConferenceTermination);
};
}

View File

@@ -0,0 +1,17 @@
/**
* The name of the {@code JitsiConnection} property which identifies the {@code JitsiConference} currently associated
* with it.
*
* FIXME: This is a hack. It was introduced to solve the following case: if a user presses hangup quickly, they may
* "leave the conference" before the actual conference was ever created. While we might have a connection in place,
* there is no conference which can be left, thus no CONFERENCE_LEFT action will ever be fired.
*
* This is problematic because the external API module used to send events to the native SDK won't know what to send.
* So, in order to detect this situation we are attaching the conference object to the connection which runs it.
*/
export const JITSI_CONNECTION_CONFERENCE_KEY = Symbol('conference');
/**
* The name of the {@code JitsiConnection} property which identifies the location URL where the connection will be made.
*/
export const JITSI_CONNECTION_URL_KEY = Symbol('url');

View File

@@ -0,0 +1,101 @@
import { IStateful } from '../app/types';
import { toState } from '../redux/functions';
import { toURLString } from '../util/uri';
import { getURLWithoutParams } from './utils';
/**
* Figures out what's the current conference URL which is supposed to indicate what conference is currently active.
* When not currently in any conference and not trying to join any then 'undefined' is returned.
*
* @param {Object|Function} stateful - Either the whole Redux state object or the Redux store's {@code getState} method.
* @returns {string|undefined}
* @private
*/
export function getCurrentConferenceUrl(stateful: IStateful) {
const state = toState(stateful);
let currentUrl;
if (isInviteURLReady(state)) {
currentUrl = toURLString(getInviteURL(state));
}
// Check if the URL doesn't end with a slash
if (currentUrl && currentUrl.substr(-1) === '/') {
currentUrl = undefined;
}
return currentUrl ? currentUrl : undefined;
}
/**
* Retrieves a simplified version of the conference/location URL stripped of URL params (i.e. Query/search and hash)
* which should be used for sending invites.
* NOTE that the method will throw an error if called too early. That is before the conference is joined or before
* the process of joining one has started. This limitation does not apply to the case when called with the URL object
* instance. Use {@link isInviteURLReady} to check if it's safe to call the method already.
*
* @param {Function|Object} stateOrGetState - The redux state or redux's {@code getState} function or the URL object
* to be stripped.
* @returns {string}
*/
export function getInviteURL(stateOrGetState: IStateful): string {
const state = toState(stateOrGetState);
let locationURL
= state instanceof URL
? state
: state['features/base/connection'].locationURL;
// If there's no locationURL on the base/connection feature try the base/config where it's set earlier.
if (!locationURL) {
locationURL = state['features/base/config'].locationURL;
}
if (!locationURL) {
throw new Error('Can not get invite URL - the app is not ready');
}
const { inviteDomain } = state['features/dynamic-branding'];
const urlWithoutParams = getURLWithoutParams(locationURL);
if (inviteDomain) {
const meetingId
= state['features/base/config'].brandingRoomAlias || urlWithoutParams.pathname.replace(/\//, '');
return `${inviteDomain}/${meetingId}`;
}
return urlWithoutParams.href;
}
/**
* Checks whether or not is safe to call the {@link getInviteURL} method already.
*
* @param {Function|Object} stateOrGetState - The redux state or redux's {@code getState} function.
* @returns {boolean}
*/
export function isInviteURLReady(stateOrGetState: IStateful): boolean {
const state = toState(stateOrGetState);
return Boolean(state['features/base/connection'].locationURL || state['features/base/config'].locationURL);
}
/**
* Converts a specific id to jid if it's not jid yet.
*
* @param {string} id - User id or jid.
* @param {Object} configHosts - The {@code hosts} part of the {@code config}
* object.
* @returns {string} A string in the form of a JID (i.e.
* {@code user@server.com}).
*/
export function toJid(id: string, { authdomain, domain }: {
anonymousdomain?: string;
authdomain?: string;
domain?: string;
focus?: string;
muc?: string;
visitorFocus?: string;
}): string {
return id.indexOf('@') >= 0 ? id : `${id}@${authdomain || domain}`;
}

View File

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

View File

@@ -0,0 +1,30 @@
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { CONNECTION_WILL_CONNECT } from './actionTypes';
/**
* The feature announced so we can distinguish jibri participants.
*
* @type {string}
*/
export const DISCO_JIBRI_FEATURE = 'http://jitsi.org/protocol/jibri';
MiddlewareRegistry.register(({ getState }) => next => action => {
switch (action.type) {
case CONNECTION_WILL_CONNECT: {
const { connection } = action;
const { iAmRecorder } = getState()['features/base/config'];
if (iAmRecorder) {
connection.addFeature(DISCO_JIBRI_FEATURE);
}
// @ts-ignore
APP.connection = connection;
break;
}
}
return next(action);
});

View File

@@ -0,0 +1,269 @@
import { SET_ROOM } from '../conference/actionTypes';
import { SET_JWT } from '../jwt/actionTypes';
import { JitsiConnectionErrors } from '../lib-jitsi-meet';
import ReducerRegistry from '../redux/ReducerRegistry';
import { assign, set } from '../redux/functions';
import {
CONNECTION_DISCONNECTED,
CONNECTION_ESTABLISHED,
CONNECTION_FAILED,
CONNECTION_WILL_CONNECT,
SET_LOCATION_URL,
SET_PREFER_VISITOR,
SHOW_CONNECTION_INFO
} from './actionTypes';
import { ConnectionFailedError } from './types';
export interface IConnectionState {
connecting?: any;
connection?: {
addFeature: Function;
disconnect: Function;
getJid: () => string;
getLogs: () => Object;
initJitsiConference: Function;
removeFeature: Function;
};
error?: ConnectionFailedError;
locationURL?: URL;
passwordRequired?: Object;
preferVisitor?: boolean;
showConnectionInfo?: boolean;
timeEstablished?: number;
}
/**
* Reduces the Redux actions of the feature base/connection.
*/
ReducerRegistry.register<IConnectionState>(
'features/base/connection',
(state = {}, action): IConnectionState => {
switch (action.type) {
case CONNECTION_DISCONNECTED:
return _connectionDisconnected(state, action);
case CONNECTION_ESTABLISHED:
return _connectionEstablished(state, action);
case CONNECTION_FAILED:
return _connectionFailed(state, action);
case CONNECTION_WILL_CONNECT:
return _connectionWillConnect(state, action);
case SET_JWT:
return _setJWT(state, action);
case SET_LOCATION_URL:
return _setLocationURL(state, action);
case SET_PREFER_VISITOR:
return assign(state, {
preferVisitor: action.preferVisitor
});
case SET_ROOM:
return _setRoom(state);
case SHOW_CONNECTION_INFO:
return _setShowConnectionInfo(state, action);
}
return state;
});
/**
* Reduces a specific Redux action CONNECTION_DISCONNECTED of the feature
* base/connection.
*
* @param {IConnectionState} state - The Redux state of the feature base/connection.
* @param {Action} action - The Redux action CONNECTION_DISCONNECTED to reduce.
* @private
* @returns {Object} The new state of the feature base/connection after the
* reduction of the specified action.
*/
function _connectionDisconnected(
state: IConnectionState,
{ connection }: { connection: Object; }) {
const connection_ = _getCurrentConnection(state);
if (connection_ !== connection) {
return state;
}
return assign(state, {
connecting: undefined,
connection: undefined,
preferVisitor: undefined,
timeEstablished: undefined
});
}
/**
* Reduces a specific Redux action CONNECTION_ESTABLISHED of the feature
* base/connection.
*
* @param {IConnectionState} state - The Redux state of the feature base/connection.
* @param {Action} action - The Redux action CONNECTION_ESTABLISHED to reduce.
* @private
* @returns {Object} The new state of the feature base/connection after the
* reduction of the specified action.
*/
function _connectionEstablished(
state: IConnectionState,
{ connection, timeEstablished }: {
connection: any;
timeEstablished: number;
}) {
return assign(state, {
connecting: undefined,
connection,
error: undefined,
passwordRequired: undefined,
timeEstablished
});
}
/**
* Reduces a specific Redux action CONNECTION_FAILED of the feature
* base/connection.
*
* @param {IConnectionState} state - The Redux state of the feature base/connection.
* @param {Action} action - The Redux action CONNECTION_FAILED to reduce.
* @private
* @returns {Object} The new state of the feature base/connection after the
* reduction of the specified action.
*/
function _connectionFailed(
state: IConnectionState,
{ connection, error }: {
connection: Object;
error: ConnectionFailedError;
}) {
const connection_ = _getCurrentConnection(state);
if (connection_ && connection_ !== connection) {
return state;
}
let preferVisitor;
if (error.name === JitsiConnectionErrors.NOT_LIVE_ERROR) {
// we want to keep the state for the moment when the meeting is live
preferVisitor = state.preferVisitor;
}
return assign(state, {
connecting: undefined,
connection: undefined,
error,
passwordRequired:
error.name === JitsiConnectionErrors.PASSWORD_REQUIRED
? connection : undefined,
preferVisitor
});
}
/**
* Reduces a specific Redux action CONNECTION_WILL_CONNECT of the feature
* base/connection.
*
* @param {IConnectionState} state - The Redux state of the feature base/connection.
* @param {Action} action - The Redux action CONNECTION_WILL_CONNECT to reduce.
* @private
* @returns {Object} The new state of the feature base/connection after the
* reduction of the specified action.
*/
function _connectionWillConnect(
state: IConnectionState,
{ connection }: { connection: Object; }) {
return assign(state, {
connecting: connection,
// We don't care if the previous connection has been closed already,
// because it's an async process and there's no guarantee if it'll be
// done before the new one is established.
connection: undefined,
error: undefined,
passwordRequired: undefined,
timeEstablished: undefined
});
}
/**
* The current (similar to getCurrentConference in base/conference/functions.any.js)
* connection which is {@code connection} or {@code connecting}.
*
* @param {IConnectionState} baseConnectionState - The current state of the
* {@code 'base/connection'} feature.
* @returns {JitsiConnection} - The current {@code JitsiConnection} if any.
* @private
*/
function _getCurrentConnection(baseConnectionState: IConnectionState): IConnectionState | undefined {
return baseConnectionState.connection || baseConnectionState.connecting;
}
/**
* Reduces a specific redux action {@link SET_JWT} of the feature
* base/connection.
*
* @param {IConnectionState} state - The redux state of the feature base/connection.
* @param {Action} action - The Redux action SET_JWT to reduce.
* @private
* @returns {Object} The new state of the feature base/connection after the
* reduction of the specified action.
*/
function _setJWT(state: IConnectionState, { preferVisitor }: { preferVisitor: boolean; }) {
return assign(state, {
preferVisitor
});
}
/**
* Reduces a specific redux action {@link SET_LOCATION_URL} of the feature
* base/connection.
*
* @param {IConnectionState} state - The redux state of the feature base/connection.
* @param {Action} action - The redux action {@code SET_LOCATION_URL} to reduce.
* @private
* @returns {Object} The new state of the feature base/connection after the
* reduction of the specified action.
*/
function _setLocationURL(
state: IConnectionState,
{ locationURL }: { locationURL?: URL; }) {
return set(state, 'locationURL', locationURL);
}
/**
* Reduces a specific redux action {@link SET_ROOM} of the feature
* base/connection.
*
* @param {IConnectionState} state - The redux state of the feature base/connection.
* @private
* @returns {Object} The new state of the feature base/connection after the
* reduction of the specified action.
*/
function _setRoom(state: IConnectionState) {
return assign(state, {
error: undefined,
passwordRequired: undefined
});
}
/**
* Reduces a specific redux action {@link SHOW_CONNECTION_INFO} of the feature
* base/connection.
*
* @param {IConnectionState} state - The redux state of the feature base/connection.
* @param {Action} action - The redux action {@code SHOW_CONNECTION_INFO} to reduce.
* @private
* @returns {Object} The new state of the feature base/connection after the
* reduction of the specified action.
*/
function _setShowConnectionInfo(
state: IConnectionState,
{ showConnectionInfo }: { showConnectionInfo: boolean; }) {
return set(state, 'showConnectionInfo', showConnectionInfo);
}

View File

@@ -0,0 +1,113 @@
/**
* The error structure passed to the {@link connectionFailed} action.
*
* Note there was an intention to make the error resemble an Error instance (to
* the extent that jitsi-meet needs it).
*/
export type ConnectionFailedError = {
/**
* The invalid credentials that were used to authenticate and the
* authentication failed.
*/
credentials?: {
/**
* The XMPP user's ID.
*/
jid: string;
/**
* The XMPP user's password.
*/
password: string;
};
/**
* The details about the connection failed event.
*/
details?: Object;
/**
* Error message.
*/
message?: string;
/**
* One of {@link JitsiConnectionError} constants (defined in
* lib-jitsi-meet).
*/
name: string;
/**
* Indicates whether this event is recoverable or not.
*/
recoverable?: boolean;
};
/**
* The value for the username or credential property.
*/
type ReplaceIceServersField = string | null;
/**
* The value for the urls property.
*/
type IceServerUrls = null | string | Array<string>;
/**
* The types of ice servers.
*/
enum IceServerType {
STUN = 'stun',
TURN = 'turn',
TURNS = 'turns'
}
/**
* Represents a single override rule.
*/
interface IReplaceIceServer {
/**
* The value the credential prop will be replaced with.
*
* NOTE: If the value is null we will remove the credential property in entry that matches the target type. If the
* value is undefined or missing we won't change the credential property in the entry that matches the target type.
*/
credential?: ReplaceIceServersField;
/**
* Target type that will be used to match the already received ice server and modify/remove it based on the values
* of credential, urls and username.
*/
targetType: IceServerType;
/**
* The value the urls prop will be replaced with.
*
* NOTE: If the value is null we will remove the whole entry that matches the target type. If the value is undefined
* or missing we won't change the urls property in the entry that matches the target type.
*/
urls?: IceServerUrls;
/**
* The value the username prop will be replaced with.
*
* NOTE: If the value is null we will remove the username property in entry that matches the target type. If the
* value is undefined or missing we won't change the username property in the entry that matches the target type.
*/
username?: ReplaceIceServersField;
}
/**
* An object with rules for changing the existing ice server configuration.
*/
export interface IIceServers {
/**
* An array of rules for replacing parts from the existing ice server configuration.
*/
replace: Array<IReplaceIceServer>;
}

View File

@@ -0,0 +1,49 @@
/**
* Gets a {@link URL} without hash and query/search params from a specific
* {@code URL}.
*
* @param {URL} url - The {@code URL} which may have hash and query/search
* params.
* @returns {URL}
*/
export function getURLWithoutParams(url: URL): URL {
const { hash, search } = url;
if ((hash && hash.length > 1) || (search && search.length > 1)) {
url = new URL(url.href); // eslint-disable-line no-param-reassign
url.hash = '';
url.search = '';
// XXX The implementation of URL at least on React Native appends ? and
// # at the end of the href which is not desired.
let { href } = url;
if (href) {
href.endsWith('#') && (href = href.substring(0, href.length - 1));
href.endsWith('?') && (href = href.substring(0, href.length - 1));
// eslint-disable-next-line no-param-reassign
url.href === href || (url = new URL(href));
}
}
return url;
}
/**
* Gets a URL string without hash and query/search params from a specific
* {@code URL}.
*
* @param {URL} url - The {@code URL} which may have hash and query/search
* params.
* @returns {string}
*/
export function getURLWithoutParamsNormalized(url: URL): string {
const urlWithoutParams = getURLWithoutParams(url).href;
if (urlWithoutParams) {
return urlWithoutParams.toLowerCase();
}
return '';
}