This commit is contained in:
84
react/features/base/connection/actionTypes.ts
Normal file
84
react/features/base/connection/actionTypes.ts
Normal 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';
|
||||
442
react/features/base/connection/actions.any.ts
Normal file
442
react/features/base/connection/actions.any.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
62
react/features/base/connection/actions.native.ts
Normal file
62
react/features/base/connection/actions.native.ts
Normal 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));
|
||||
}
|
||||
90
react/features/base/connection/actions.web.ts
Normal file
90
react/features/base/connection/actions.web.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
17
react/features/base/connection/constants.ts
Normal file
17
react/features/base/connection/constants.ts
Normal 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');
|
||||
101
react/features/base/connection/functions.ts
Normal file
101
react/features/base/connection/functions.ts
Normal 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}`;
|
||||
}
|
||||
3
react/features/base/connection/logger.ts
Normal file
3
react/features/base/connection/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/connection');
|
||||
30
react/features/base/connection/middleware.web.ts
Normal file
30
react/features/base/connection/middleware.web.ts
Normal 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);
|
||||
});
|
||||
269
react/features/base/connection/reducer.ts
Normal file
269
react/features/base/connection/reducer.ts
Normal 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);
|
||||
}
|
||||
113
react/features/base/connection/types.ts
Normal file
113
react/features/base/connection/types.ts
Normal 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>;
|
||||
}
|
||||
|
||||
49
react/features/base/connection/utils.ts
Normal file
49
react/features/base/connection/utils.ts
Normal 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 '';
|
||||
}
|
||||
Reference in New Issue
Block a user