This commit is contained in:
33
react/features/base/app/actionTypes.ts
Normal file
33
react/features/base/app/actionTypes.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* The type of (redux) action which signals that a specific App will mount (in
|
||||
* React terms).
|
||||
*
|
||||
* {
|
||||
* type: APP_WILL_MOUNT,
|
||||
* app: App
|
||||
* }
|
||||
*/
|
||||
export const APP_WILL_MOUNT = 'APP_WILL_MOUNT';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that a specific App will unmount (in
|
||||
* React terms).
|
||||
*
|
||||
* {
|
||||
* type: APP_WILL_UNMOUNT,
|
||||
* app: App
|
||||
* }
|
||||
*/
|
||||
export const APP_WILL_UNMOUNT = 'APP_WILL_UNMOUNT';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that a specific App will navigate using a route (in
|
||||
* React terms).
|
||||
*
|
||||
* {
|
||||
* type: APP_WILL_NAVIGATE,
|
||||
* app: App,
|
||||
* route: Route
|
||||
* }
|
||||
*/
|
||||
export const APP_WILL_NAVIGATE = 'APP_WILL_NAVIGATE';
|
||||
69
react/features/base/app/actions.ts
Normal file
69
react/features/base/app/actions.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { IStore } from '../../app/types';
|
||||
|
||||
import {
|
||||
APP_WILL_MOUNT,
|
||||
APP_WILL_NAVIGATE,
|
||||
APP_WILL_UNMOUNT
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Signals that a specific App will mount (in the terms of React).
|
||||
*
|
||||
* @param {App} app - The App which will mount.
|
||||
* @returns {{
|
||||
* type: APP_WILL_MOUNT,
|
||||
* app: App
|
||||
* }}
|
||||
*/
|
||||
export function appWillMount(app: Object) {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
// TODO There was a redux action creator appInit which I did not like
|
||||
// because we already had the redux action creator appWillMount and,
|
||||
// respectively, the redux action APP_WILL_MOUNT. So I set out to remove
|
||||
// appInit and managed to move everything it was doing but the
|
||||
// following. Which is not extremely bad because we haven't moved the
|
||||
// API module into its own feature yet so we're bound to work on that in
|
||||
// the future.
|
||||
typeof APP === 'object' && APP.API.init();
|
||||
|
||||
dispatch({
|
||||
type: APP_WILL_MOUNT,
|
||||
app
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that a specific App will unmount (in the terms of React).
|
||||
*
|
||||
* @param {App} app - The App which will unmount.
|
||||
* @returns {{
|
||||
* type: APP_WILL_UNMOUNT,
|
||||
* app: App
|
||||
* }}
|
||||
*/
|
||||
export function appWillUnmount(app: Object) {
|
||||
return {
|
||||
type: APP_WILL_UNMOUNT,
|
||||
app
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that a specific App will navigate (in the terms of React).
|
||||
*
|
||||
* @param {App} app - The App which will navigate.
|
||||
* @param {Object} route - The route which will be used.
|
||||
* @returns {{
|
||||
* type: APP_WILL_NAVIGATE,
|
||||
* app: App,
|
||||
* route: Object
|
||||
* }}
|
||||
*/
|
||||
export function appWillNavigate(app: Object, route: Object) {
|
||||
return {
|
||||
type: APP_WILL_NAVIGATE,
|
||||
app,
|
||||
route
|
||||
};
|
||||
}
|
||||
283
react/features/base/app/components/BaseApp.tsx
Normal file
283
react/features/base/app/components/BaseApp.tsx
Normal file
@@ -0,0 +1,283 @@
|
||||
// @ts-expect-error
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import React, { Component, ComponentType, Fragment } from 'react';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
import { compose, createStore } from 'redux';
|
||||
import Thunk from 'redux-thunk';
|
||||
|
||||
import { IStore } from '../../../app/types';
|
||||
import i18next from '../../i18n/i18next';
|
||||
import MiddlewareRegistry from '../../redux/MiddlewareRegistry';
|
||||
import PersistenceRegistry from '../../redux/PersistenceRegistry';
|
||||
import ReducerRegistry from '../../redux/ReducerRegistry';
|
||||
import StateListenerRegistry from '../../redux/StateListenerRegistry';
|
||||
import SoundCollection from '../../sounds/components/SoundCollection';
|
||||
import { appWillMount, appWillUnmount } from '../actions';
|
||||
import logger from '../logger';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link BaseApp}.
|
||||
*/
|
||||
interface IState {
|
||||
|
||||
/**
|
||||
* The {@code Route} rendered by the {@code BaseApp}.
|
||||
*/
|
||||
route: {
|
||||
component?: ComponentType;
|
||||
props?: Object;
|
||||
};
|
||||
|
||||
/**
|
||||
* The redux store used by the {@code BaseApp}.
|
||||
*/
|
||||
store?: IStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base (abstract) class for main App component.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export default class BaseApp<P> extends Component<P, IState> {
|
||||
/**
|
||||
* The deferred for the initialisation {{promise, resolve, reject}}.
|
||||
*/
|
||||
_init: PromiseWithResolvers<any>;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code BaseApp} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
route: {},
|
||||
store: undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the app.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override async componentDidMount() {
|
||||
/**
|
||||
* Make the mobile {@code BaseApp} wait until the {@code AsyncStorage}
|
||||
* implementation of {@code Storage} initializes fully.
|
||||
*
|
||||
* @private
|
||||
* @see {@link #_initStorage}
|
||||
* @type {Promise}
|
||||
*/
|
||||
this._init = Promise.withResolvers();
|
||||
|
||||
try {
|
||||
await this._initStorage();
|
||||
|
||||
const setStatePromise = new Promise(resolve => {
|
||||
this.setState({
|
||||
// @ts-ignore
|
||||
store: this._createStore()
|
||||
}, resolve);
|
||||
});
|
||||
|
||||
await setStatePromise;
|
||||
|
||||
await this._extraInit();
|
||||
} catch (err) {
|
||||
/* BaseApp should always initialize! */
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
this.state.store?.dispatch(appWillMount(this));
|
||||
|
||||
// @ts-ignore
|
||||
this._init.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* De-initializes the app.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentWillUnmount() {
|
||||
this.state.store?.dispatch(appWillUnmount(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs for errors that were not caught.
|
||||
*
|
||||
* @param {Error} error - The error that was thrown.
|
||||
* @param {Object} info - Info about the error(stack trace);.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentDidCatch(error: Error, info: Object) {
|
||||
logger.error(error, info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delays this {@code BaseApp}'s startup until the {@code Storage}
|
||||
* implementation of {@code localStorage} initializes. While the
|
||||
* initialization is instantaneous on Web (with Web Storage API), it is
|
||||
* asynchronous on mobile/react-native.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_initStorage(): Promise<any> {
|
||||
const _initializing = jitsiLocalStorage.getItem('_initializing');
|
||||
|
||||
return _initializing || Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra initialisation that subclasses might require.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_extraInit() {
|
||||
// To be implemented by subclass.
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const { route: { component, props }, store } = this.state;
|
||||
|
||||
if (store) {
|
||||
return (
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
{/* @ts-ignore */}
|
||||
<Provider store = { store }>
|
||||
<Fragment>
|
||||
{ this._createMainElement(component, props) }
|
||||
<SoundCollection />
|
||||
{ this._createExtraElement() }
|
||||
{ this._renderDialogContainer() }
|
||||
</Fragment>
|
||||
</Provider>
|
||||
</I18nextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an extra {@link ReactElement}s to be added (unconditionally)
|
||||
* alongside the main element.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
* @abstract
|
||||
* @protected
|
||||
*/
|
||||
_createExtraElement(): React.ReactElement | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link ReactElement} from the specified component, the
|
||||
* specified props and the props of this {@code AbstractApp} which are
|
||||
* suitable for propagation to the children of this {@code Component}.
|
||||
*
|
||||
* @param {Component} component - The component from which the
|
||||
* {@code ReactElement} is to be created.
|
||||
* @param {Object} props - The read-only React {@code Component} props with
|
||||
* which the {@code ReactElement} is to be initialized.
|
||||
* @returns {ReactElement}
|
||||
* @protected
|
||||
*/
|
||||
_createMainElement(component?: ComponentType, props?: Object) {
|
||||
return component ? React.createElement(component, props || {}) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new redux store instance suitable for use by this
|
||||
* {@code AbstractApp}.
|
||||
*
|
||||
* @private
|
||||
* @returns {Store} - A new redux store instance suitable for use by
|
||||
* this {@code AbstractApp}.
|
||||
*/
|
||||
_createStore() {
|
||||
// Create combined reducer from all reducers in ReducerRegistry.
|
||||
const reducer = ReducerRegistry.combineReducers();
|
||||
|
||||
// Apply all registered middleware from the MiddlewareRegistry and
|
||||
// additional 3rd party middleware:
|
||||
// - Thunk - allows us to dispatch async actions easily. For more info
|
||||
// @see https://github.com/gaearon/redux-thunk.
|
||||
const middleware = MiddlewareRegistry.applyMiddleware(Thunk);
|
||||
|
||||
// @ts-ignore
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
const store = createStore(reducer, PersistenceRegistry.getPersistedState(), composeEnhancers(middleware));
|
||||
|
||||
// StateListenerRegistry
|
||||
StateListenerRegistry.subscribe(store);
|
||||
|
||||
// This is temporary workaround to be able to dispatch actions from
|
||||
// non-reactified parts of the code (conference.js for example).
|
||||
// Don't use in the react code!!!
|
||||
// FIXME: remove when the reactification is finished!
|
||||
if (typeof APP !== 'undefined') {
|
||||
// @ts-ignore
|
||||
APP.store = store;
|
||||
}
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to a specific Route.
|
||||
*
|
||||
* @param {Route} route - The Route to which to navigate.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_navigate(route: {
|
||||
component?: ComponentType<any>;
|
||||
href?: string;
|
||||
props?: Object;
|
||||
}): Promise<any> {
|
||||
if (isEqual(route, this.state.route)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (route.href) {
|
||||
// This navigation requires loading a new URL in the browser.
|
||||
window.location.href = route.href;
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// XXX React's setState is asynchronous which means that the value of
|
||||
// this.state.route above may not even be correct. If the check is
|
||||
// performed before setState completes, the app may not navigate to the
|
||||
// expected route. In order to mitigate the problem, _navigate was
|
||||
// changed to return a Promise.
|
||||
return new Promise(resolve => { // @ts-ignore
|
||||
this.setState({ route }, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the platform specific dialog container.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderDialogContainer(): React.ReactElement | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
28
react/features/base/app/functions.ts
Normal file
28
react/features/base/app/functions.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { toState } from '../redux/functions';
|
||||
|
||||
import { IStateful } from './types';
|
||||
|
||||
/**
|
||||
* Gets the value of a specific React {@code Component} prop of the currently
|
||||
* mounted {@link App}.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store or {@code getState}
|
||||
* function.
|
||||
* @param {string} propName - The name of the React {@code Component} prop of
|
||||
* the currently mounted {@code App} to get.
|
||||
* @returns {*} The value of the specified React {@code Component} prop of the
|
||||
* currently mounted {@code App}.
|
||||
*/
|
||||
export function getAppProp(stateful: IStateful, propName: string) {
|
||||
const state = toState(stateful)['features/base/app'];
|
||||
|
||||
if (state) {
|
||||
const { app } = state;
|
||||
|
||||
if (app) {
|
||||
return app.props[propName];
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
3
react/features/base/app/logger.ts
Normal file
3
react/features/base/app/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/app');
|
||||
54
react/features/base/app/middleware.web.ts
Normal file
54
react/features/base/app/middleware.web.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import { isEmbedded } from '../util/embedUtils';
|
||||
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
|
||||
import logger from './logger';
|
||||
|
||||
|
||||
/**
|
||||
* Experimental feature to monitor CPU pressure.
|
||||
*/
|
||||
let pressureObserver: typeof window.PressureObserver;
|
||||
|
||||
/**
|
||||
* Middleware which intercepts app actions to handle changes to the related state.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(() => (next: Function) => (action: AnyAction) => {
|
||||
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT: {
|
||||
// Disable it inside an iframe until Google fixes the origin trial for 3rd party sources:
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1504167
|
||||
if (!isEmbedded() && 'PressureObserver' in window) {
|
||||
pressureObserver = new window.PressureObserver(
|
||||
(records: typeof window.PressureRecord) => {
|
||||
logger.info('Compute pressure state changed:', JSON.stringify(records));
|
||||
APP.API.notifyComputePressureChanged(records);
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
pressureObserver
|
||||
.observe('cpu', { sampleInterval: 30_000 })
|
||||
.catch((e: any) => logger.error('CPU pressure observer failed to start', e));
|
||||
} catch (e: any) {
|
||||
logger.error('CPU pressure observer failed to start', e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case APP_WILL_UNMOUNT: {
|
||||
if (pressureObserver) {
|
||||
pressureObserver.unobserve('cpu');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
34
react/features/base/app/reducer.ts
Normal file
34
react/features/base/app/reducer.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import ReducerRegistry from '../redux/ReducerRegistry';
|
||||
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
|
||||
|
||||
export interface IAppState {
|
||||
app?: any;
|
||||
}
|
||||
|
||||
ReducerRegistry.register<IAppState>('features/base/app', (state = {}, action): IAppState => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT: {
|
||||
const { app } = action;
|
||||
|
||||
if (state.app !== app) {
|
||||
return {
|
||||
...state,
|
||||
app
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case APP_WILL_UNMOUNT:
|
||||
if (state.app === action.app) {
|
||||
return {
|
||||
...state,
|
||||
app: undefined
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
3
react/features/base/app/types.ts
Normal file
3
react/features/base/app/types.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
|
||||
export type IStateful = (() => IReduxState) | IStore | IReduxState;
|
||||
10
react/features/base/audio-only/actionTypes.ts
Normal file
10
react/features/base/audio-only/actionTypes.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* The type of (redux) action which sets the audio-only flag for the current
|
||||
* conference.
|
||||
*
|
||||
* {
|
||||
* type: SET_AUDIO_ONLY,
|
||||
* audioOnly: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_AUDIO_ONLY = 'SET_AUDIO_ONLY';
|
||||
51
react/features/base/audio-only/actions.ts
Normal file
51
react/features/base/audio-only/actions.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { createAudioOnlyChangedEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IStore } from '../../app/types';
|
||||
|
||||
import { SET_AUDIO_ONLY } from './actionTypes';
|
||||
import logger from './logger';
|
||||
|
||||
|
||||
/**
|
||||
* Sets the audio-only flag for the current JitsiConference.
|
||||
*
|
||||
* @param {boolean} audioOnly - True if the conference should be audio only; false, otherwise.
|
||||
* @returns {{
|
||||
* type: SET_AUDIO_ONLY,
|
||||
* audioOnly: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setAudioOnly(audioOnly: boolean) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const { enabled: oldValue } = getState()['features/base/audio-only'];
|
||||
|
||||
if (oldValue !== audioOnly) {
|
||||
sendAnalytics(createAudioOnlyChangedEvent(audioOnly));
|
||||
logger.log(`Audio-only ${audioOnly ? 'enabled' : 'disabled'}`);
|
||||
|
||||
dispatch({
|
||||
type: SET_AUDIO_ONLY,
|
||||
audioOnly
|
||||
});
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
// TODO This should be a temporary solution that lasts only until video
|
||||
// tracks and all ui is moved into react/redux on the web.
|
||||
APP.conference.onToggleAudioOnly();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the audio-only flag for the current JitsiConference.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function toggleAudioOnly() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const { enabled } = getState()['features/base/audio-only'];
|
||||
|
||||
dispatch(setAudioOnly(!enabled));
|
||||
};
|
||||
}
|
||||
3
react/features/base/audio-only/logger.ts
Normal file
3
react/features/base/audio-only/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/audio-only');
|
||||
25
react/features/base/audio-only/reducer.ts
Normal file
25
react/features/base/audio-only/reducer.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import ReducerRegistry from '../redux/ReducerRegistry';
|
||||
|
||||
import { SET_AUDIO_ONLY } from './actionTypes';
|
||||
|
||||
export interface IAudioOnlyState {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
enabled: false
|
||||
};
|
||||
|
||||
|
||||
ReducerRegistry.register<IAudioOnlyState>('features/base/audio-only',
|
||||
(state = DEFAULT_STATE, action): IAudioOnlyState => {
|
||||
switch (action.type) {
|
||||
case SET_AUDIO_ONLY:
|
||||
return {
|
||||
...state,
|
||||
enabled: action.audioOnly
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
||||
295
react/features/base/avatar/components/Avatar.tsx
Normal file
295
react/features/base/avatar/components/Avatar.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { IconUser } from '../../icons/svg';
|
||||
import { getParticipantById } from '../../participants/functions';
|
||||
import { IParticipant } from '../../participants/types';
|
||||
import { getAvatarColor, getInitials, isCORSAvatarURL } from '../functions';
|
||||
import { IAvatarProps as AbstractProps } from '../types';
|
||||
|
||||
import { StatelessAvatar } from './';
|
||||
|
||||
export interface IProps {
|
||||
|
||||
/**
|
||||
* The URL patterns for URLs that needs to be handled with CORS.
|
||||
*/
|
||||
_corsAvatarURLs?: Array<string>;
|
||||
|
||||
/**
|
||||
* Custom avatar backgrounds from branding.
|
||||
*/
|
||||
_customAvatarBackgrounds?: Array<string>;
|
||||
|
||||
/**
|
||||
* The string we base the initials on (this is generated from a list of precedences).
|
||||
*/
|
||||
_initialsBase?: string;
|
||||
|
||||
/**
|
||||
* An URL that we validated that it can be loaded.
|
||||
*/
|
||||
_loadableAvatarUrl?: string;
|
||||
|
||||
/**
|
||||
* Indicates whether _loadableAvatarUrl should use CORS or not.
|
||||
*/
|
||||
_loadableAvatarUrlUseCORS?: boolean;
|
||||
|
||||
/**
|
||||
* A prop to maintain compatibility with web.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* A string to override the initials to generate a color of. This is handy if you don't want to make
|
||||
* the background color match the string that the initials are generated from.
|
||||
*/
|
||||
colorBase?: string;
|
||||
|
||||
/**
|
||||
* Indicates the default icon for the avatar.
|
||||
*/
|
||||
defaultIcon?: string;
|
||||
|
||||
/**
|
||||
* Display name of the entity to render an avatar for (if any). This is handy when we need
|
||||
* an avatar for a non-participant entity (e.g. A recent list item).
|
||||
*/
|
||||
displayName?: string;
|
||||
|
||||
/**
|
||||
* Whether or not to update the background color of the avatar.
|
||||
*/
|
||||
dynamicColor?: boolean;
|
||||
|
||||
/**
|
||||
* ID of the element, if any.
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* The ID of the participant to render an avatar for (if it's a participant avatar).
|
||||
*/
|
||||
participantId?: string;
|
||||
|
||||
/**
|
||||
* The size of the avatar.
|
||||
*/
|
||||
size?: number;
|
||||
|
||||
/**
|
||||
* One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
/**
|
||||
* TestId of the element, if any.
|
||||
*/
|
||||
testId?: string;
|
||||
|
||||
/**
|
||||
* URL of the avatar, if any.
|
||||
*/
|
||||
url?: string;
|
||||
|
||||
/**
|
||||
* Indicates whether to load the avatar using CORS or not.
|
||||
*/
|
||||
useCORS?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
avatarFailed: boolean;
|
||||
isUsingCORS: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SIZE = 65;
|
||||
|
||||
/**
|
||||
* Implements a class to render avatars in the app.
|
||||
*/
|
||||
class Avatar<P extends IProps> extends PureComponent<P, IState> {
|
||||
/**
|
||||
* Default values for {@code Avatar} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
defaultIcon: IconUser,
|
||||
dynamicColor: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiates a new {@code Component}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
const {
|
||||
_corsAvatarURLs,
|
||||
url,
|
||||
useCORS
|
||||
} = props;
|
||||
|
||||
this.state = {
|
||||
avatarFailed: false,
|
||||
isUsingCORS: Boolean(useCORS) || Boolean(url && isCORSAvatarURL(url, _corsAvatarURLs))
|
||||
};
|
||||
|
||||
this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#componentDidUpdate}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentDidUpdate(prevProps: P) {
|
||||
const { _corsAvatarURLs, url } = this.props;
|
||||
|
||||
if (prevProps.url !== url) {
|
||||
|
||||
// URI changed, so we need to try to fetch it again.
|
||||
// Eslint doesn't like this statement, but based on the React doc, it's safe if it's
|
||||
// wrapped in a condition: https://reactjs.org/docs/react-component.html#componentdidupdate
|
||||
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({
|
||||
avatarFailed: false,
|
||||
isUsingCORS: Boolean(this.props.useCORS) || Boolean(url && isCORSAvatarURL(url, _corsAvatarURLs))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Componenr#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
_customAvatarBackgrounds,
|
||||
_initialsBase,
|
||||
_loadableAvatarUrl,
|
||||
_loadableAvatarUrlUseCORS,
|
||||
className,
|
||||
colorBase,
|
||||
defaultIcon,
|
||||
dynamicColor,
|
||||
id,
|
||||
size,
|
||||
status,
|
||||
testId,
|
||||
url
|
||||
} = this.props;
|
||||
const { avatarFailed, isUsingCORS } = this.state;
|
||||
|
||||
const avatarProps: AbstractProps & {
|
||||
className?: string;
|
||||
iconUser?: any;
|
||||
id?: string;
|
||||
status?: string;
|
||||
testId?: string;
|
||||
url?: string;
|
||||
useCORS?: boolean;
|
||||
} = {
|
||||
className,
|
||||
color: undefined,
|
||||
id,
|
||||
initials: undefined,
|
||||
onAvatarLoadError: undefined,
|
||||
onAvatarLoadErrorParams: undefined,
|
||||
size,
|
||||
status,
|
||||
testId,
|
||||
url: undefined,
|
||||
useCORS: isUsingCORS
|
||||
};
|
||||
|
||||
// _loadableAvatarUrl is validated that it can be loaded, but uri (if present) is not, so
|
||||
// we still need to do a check for that. And an explicitly provided URI is higher priority than
|
||||
// an avatar URL anyhow.
|
||||
const useReduxLoadableAvatarURL = avatarFailed || !url;
|
||||
const effectiveURL = useReduxLoadableAvatarURL ? _loadableAvatarUrl : url;
|
||||
|
||||
if (effectiveURL) {
|
||||
avatarProps.onAvatarLoadError = this._onAvatarLoadError;
|
||||
if (useReduxLoadableAvatarURL) {
|
||||
avatarProps.onAvatarLoadErrorParams = { dontRetry: true };
|
||||
avatarProps.useCORS = _loadableAvatarUrlUseCORS;
|
||||
}
|
||||
avatarProps.url = effectiveURL;
|
||||
}
|
||||
|
||||
const initials = getInitials(_initialsBase);
|
||||
|
||||
if (initials) {
|
||||
if (dynamicColor) {
|
||||
avatarProps.color = getAvatarColor(colorBase || _initialsBase, _customAvatarBackgrounds ?? []);
|
||||
}
|
||||
|
||||
avatarProps.initials = initials;
|
||||
}
|
||||
|
||||
if (navigator.product !== 'ReactNative') {
|
||||
avatarProps.iconUser = defaultIcon;
|
||||
}
|
||||
|
||||
return (
|
||||
<StatelessAvatar
|
||||
{ ...avatarProps } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to handle the error while loading of the avatar URI.
|
||||
*
|
||||
* @param {Object} params - An object with parameters.
|
||||
* @param {boolean} params.dontRetry - If false we will retry to load the Avatar with different CORS mode.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onAvatarLoadError(params: { dontRetry?: boolean; } = {}) {
|
||||
const { dontRetry = false } = params;
|
||||
|
||||
if (Boolean(this.props.useCORS) === this.state.isUsingCORS && !dontRetry) {
|
||||
// try different mode of loading the avatar.
|
||||
this.setState({
|
||||
isUsingCORS: !this.state.isUsingCORS
|
||||
});
|
||||
} else {
|
||||
// we already have tried loading the avatar with and without CORS and it failed.
|
||||
this.setState({
|
||||
avatarFailed: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {IProps} ownProps - The own props of the component.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState, ownProps: IProps) {
|
||||
const { colorBase, displayName, participantId } = ownProps;
|
||||
const _participant: IParticipant | undefined = participantId ? getParticipantById(state, participantId) : undefined;
|
||||
const _initialsBase = _participant?.name ?? displayName;
|
||||
const { corsAvatarURLs } = state['features/base/config'];
|
||||
|
||||
return {
|
||||
_customAvatarBackgrounds: state['features/dynamic-branding'].avatarBackgrounds,
|
||||
_corsAvatarURLs: corsAvatarURLs,
|
||||
_initialsBase,
|
||||
_loadableAvatarUrl: _participant?.loadableAvatarUrl,
|
||||
_loadableAvatarUrlUseCORS: _participant?.loadableAvatarUrlUseCORS,
|
||||
colorBase
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(Avatar);
|
||||
1
react/features/base/avatar/components/index.native.ts
Normal file
1
react/features/base/avatar/components/index.native.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as StatelessAvatar } from './native/StatelessAvatar';
|
||||
1
react/features/base/avatar/components/index.web.ts
Normal file
1
react/features/base/avatar/components/index.web.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as StatelessAvatar } from './web/StatelessAvatar';
|
||||
200
react/features/base/avatar/components/native/StatelessAvatar.tsx
Normal file
200
react/features/base/avatar/components/native/StatelessAvatar.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Image, Text, TextStyle, View, ViewStyle } from 'react-native';
|
||||
|
||||
import Icon from '../../../icons/components/Icon';
|
||||
import { StyleType } from '../../../styles/functions.native';
|
||||
import { isIcon } from '../../functions';
|
||||
import { IAvatarProps } from '../../types';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const DEFAULT_AVATAR = require('../../../../../../images/avatar.png');
|
||||
|
||||
interface IProps extends IAvatarProps {
|
||||
|
||||
/**
|
||||
* One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
/**
|
||||
* External style passed to the component.
|
||||
*/
|
||||
style?: StyleType;
|
||||
|
||||
/**
|
||||
* The URL of the avatar to render.
|
||||
*/
|
||||
url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a stateless avatar component that renders an avatar purely from what gets passed through
|
||||
* props.
|
||||
*/
|
||||
export default class StatelessAvatar extends Component<IProps> {
|
||||
|
||||
/**
|
||||
* Instantiates a new {@code Component}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { initials, size, style, url } = this.props;
|
||||
|
||||
let avatar;
|
||||
|
||||
if (isIcon(url)) {
|
||||
avatar = this._renderIconAvatar(url);
|
||||
} else if (url) {
|
||||
avatar = this._renderURLAvatar();
|
||||
} else if (initials) {
|
||||
avatar = this._renderInitialsAvatar();
|
||||
} else {
|
||||
avatar = this._renderDefaultAvatar();
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View
|
||||
style = { [
|
||||
styles.avatarContainer(size) as ViewStyle,
|
||||
style
|
||||
] }>
|
||||
{ avatar }
|
||||
</View>
|
||||
{ this._renderAvatarStatus() }
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a badge representing the avatar status.
|
||||
*
|
||||
* @returns {React$Elementaa}
|
||||
*/
|
||||
_renderAvatarStatus() {
|
||||
const { size, status } = this.props;
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style = { styles.badgeContainer }>
|
||||
<View style = { styles.badge(size, status) as ViewStyle } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the default avatar.
|
||||
*
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderDefaultAvatar() {
|
||||
const { size } = this.props;
|
||||
|
||||
return (
|
||||
<Image
|
||||
source = { DEFAULT_AVATAR }
|
||||
style = { [
|
||||
styles.avatarContent(size),
|
||||
styles.staticAvatar
|
||||
] } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the icon avatar.
|
||||
*
|
||||
* @param {Object} icon - The icon component to render.
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderIconAvatar(icon: Function) {
|
||||
const { color, size } = this.props;
|
||||
|
||||
return (
|
||||
<View
|
||||
style = { [
|
||||
styles.initialsContainer as ViewStyle,
|
||||
{
|
||||
backgroundColor: color
|
||||
}
|
||||
] }>
|
||||
<Icon
|
||||
src = { icon }
|
||||
style = { styles.initialsText(size) } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the initials-based avatar.
|
||||
*
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderInitialsAvatar() {
|
||||
const { color, initials, size } = this.props;
|
||||
|
||||
return (
|
||||
<View
|
||||
style = { [
|
||||
styles.initialsContainer as ViewStyle,
|
||||
{
|
||||
backgroundColor: color
|
||||
}
|
||||
] }>
|
||||
<Text style = { styles.initialsText(size) as TextStyle }> { initials } </Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the url-based avatar.
|
||||
*
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderURLAvatar() {
|
||||
const { onAvatarLoadError, size, url } = this.props;
|
||||
|
||||
return (
|
||||
<Image
|
||||
defaultSource = { DEFAULT_AVATAR }
|
||||
|
||||
// @ts-ignore
|
||||
onError = { onAvatarLoadError }
|
||||
resizeMode = 'cover'
|
||||
source = {{ uri: url }}
|
||||
style = { styles.avatarContent(size) } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles avatar load errors.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onAvatarLoadError() {
|
||||
const { onAvatarLoadError, onAvatarLoadErrorParams = {} } = this.props;
|
||||
|
||||
if (onAvatarLoadError) {
|
||||
onAvatarLoadError({
|
||||
...onAvatarLoadErrorParams,
|
||||
dontRetry: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
82
react/features/base/avatar/components/native/styles.ts
Normal file
82
react/features/base/avatar/components/native/styles.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { ColorPalette } from '../../../styles/components/styles/ColorPalette';
|
||||
import { PRESENCE_AVAILABLE_COLOR, PRESENCE_AWAY_COLOR, PRESENCE_BUSY_COLOR, PRESENCE_IDLE_COLOR } from '../styles';
|
||||
|
||||
const DEFAULT_SIZE = 65;
|
||||
|
||||
/**
|
||||
* The styles of the feature base/participants.
|
||||
*/
|
||||
export default {
|
||||
|
||||
avatarContainer: (size: number = DEFAULT_SIZE) => {
|
||||
return {
|
||||
alignItems: 'center',
|
||||
borderRadius: size / 2,
|
||||
height: size,
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
width: size
|
||||
};
|
||||
},
|
||||
|
||||
avatarContent: (size: number = DEFAULT_SIZE) => {
|
||||
return {
|
||||
height: size,
|
||||
width: size
|
||||
};
|
||||
},
|
||||
|
||||
badge: (size: number = DEFAULT_SIZE, status: string) => {
|
||||
let color;
|
||||
|
||||
switch (status) {
|
||||
case 'available':
|
||||
color = PRESENCE_AVAILABLE_COLOR;
|
||||
break;
|
||||
case 'away':
|
||||
color = PRESENCE_AWAY_COLOR;
|
||||
break;
|
||||
case 'busy':
|
||||
color = PRESENCE_BUSY_COLOR;
|
||||
break;
|
||||
case 'idle':
|
||||
color = PRESENCE_IDLE_COLOR;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
backgroundColor: color,
|
||||
borderRadius: size / 2,
|
||||
bottom: 0,
|
||||
height: size * 0.3,
|
||||
position: 'absolute',
|
||||
width: size * 0.3
|
||||
};
|
||||
},
|
||||
|
||||
badgeContainer: {
|
||||
...StyleSheet.absoluteFillObject
|
||||
},
|
||||
|
||||
initialsContainer: {
|
||||
alignItems: 'center',
|
||||
alignSelf: 'stretch',
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
initialsText: (size: number = DEFAULT_SIZE) => {
|
||||
return {
|
||||
color: 'white',
|
||||
fontSize: size * 0.45,
|
||||
fontWeight: '100'
|
||||
};
|
||||
},
|
||||
|
||||
staticAvatar: {
|
||||
backgroundColor: ColorPalette.lightGrey,
|
||||
opacity: 0.4
|
||||
}
|
||||
};
|
||||
5
react/features/base/avatar/components/styles.ts
Normal file
5
react/features/base/avatar/components/styles.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// Colors for avatar status badge
|
||||
export const PRESENCE_AVAILABLE_COLOR = 'rgb(110, 176, 5)';
|
||||
export const PRESENCE_AWAY_COLOR = 'rgb(250, 201, 20)';
|
||||
export const PRESENCE_BUSY_COLOR = 'rgb(233, 0, 27)';
|
||||
export const PRESENCE_IDLE_COLOR = 'rgb(172, 172, 172)';
|
||||
220
react/features/base/avatar/components/web/StatelessAvatar.tsx
Normal file
220
react/features/base/avatar/components/web/StatelessAvatar.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Icon from '../../../icons/components/Icon';
|
||||
import { pixelsToRem } from '../../../ui/functions.any';
|
||||
import { isIcon } from '../../functions';
|
||||
import { IAvatarProps } from '../../types';
|
||||
import { PRESENCE_AVAILABLE_COLOR, PRESENCE_AWAY_COLOR, PRESENCE_BUSY_COLOR, PRESENCE_IDLE_COLOR } from '../styles';
|
||||
|
||||
interface IProps extends IAvatarProps {
|
||||
|
||||
/**
|
||||
* External class name passed through props.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* The default avatar URL if we want to override the app bundled one (e.g. AlwaysOnTop).
|
||||
*/
|
||||
defaultAvatar?: string;
|
||||
|
||||
/**
|
||||
* ID of the component to be rendered.
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
/**
|
||||
* TestId of the element, if any.
|
||||
*/
|
||||
testId?: string;
|
||||
|
||||
/**
|
||||
* The URL of the avatar to render.
|
||||
*/
|
||||
url?: string | Function;
|
||||
|
||||
/**
|
||||
* Indicates whether to load the avatar using CORS or not.
|
||||
*/
|
||||
useCORS?: boolean;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
avatar: {
|
||||
backgroundColor: '#AAA',
|
||||
borderRadius: '50%',
|
||||
color: theme.palette?.text01 || '#fff',
|
||||
...(theme.typography?.heading1 ?? {}),
|
||||
fontSize: 'inherit',
|
||||
objectFit: 'cover',
|
||||
textAlign: 'center',
|
||||
overflow: 'hidden',
|
||||
|
||||
'&.avatar-small': {
|
||||
height: '28px !important',
|
||||
width: '28px !important'
|
||||
},
|
||||
|
||||
'&.avatar-xsmall': {
|
||||
height: '16px !important',
|
||||
width: '16px !important'
|
||||
},
|
||||
|
||||
'& .jitsi-icon': {
|
||||
transform: 'translateY(50%)'
|
||||
},
|
||||
|
||||
'& .avatar-svg': {
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}
|
||||
},
|
||||
|
||||
initialsContainer: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
|
||||
badge: {
|
||||
position: 'relative',
|
||||
|
||||
'&.avatar-badge:after': {
|
||||
borderRadius: '50%',
|
||||
content: '""',
|
||||
display: 'block',
|
||||
height: '35%',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
width: '35%'
|
||||
},
|
||||
|
||||
'&.avatar-badge-available:after': {
|
||||
backgroundColor: PRESENCE_AVAILABLE_COLOR
|
||||
},
|
||||
|
||||
'&.avatar-badge-away:after': {
|
||||
backgroundColor: PRESENCE_AWAY_COLOR
|
||||
},
|
||||
|
||||
'&.avatar-badge-busy:after': {
|
||||
backgroundColor: PRESENCE_BUSY_COLOR
|
||||
},
|
||||
|
||||
'&.avatar-badge-idle:after': {
|
||||
backgroundColor: PRESENCE_IDLE_COLOR
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const StatelessAvatar = ({
|
||||
className,
|
||||
color,
|
||||
iconUser,
|
||||
id,
|
||||
initials,
|
||||
onAvatarLoadError,
|
||||
onAvatarLoadErrorParams,
|
||||
size,
|
||||
status,
|
||||
testId,
|
||||
url,
|
||||
useCORS
|
||||
}: IProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
const _getAvatarStyle = (backgroundColor?: string) => {
|
||||
return {
|
||||
background: backgroundColor || undefined,
|
||||
fontSize: size ? pixelsToRem(size * 0.4) : '180%',
|
||||
height: size || '100%',
|
||||
width: size || '100%'
|
||||
};
|
||||
};
|
||||
|
||||
const _getAvatarClassName = (additional?: string) => cx('avatar', additional, className, classes.avatar);
|
||||
|
||||
const _getBadgeClassName = () => {
|
||||
if (status) {
|
||||
return cx('avatar-badge', `avatar-badge-${status}`, classes.badge);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const _onAvatarLoadError = useCallback(() => {
|
||||
if (typeof onAvatarLoadError === 'function') {
|
||||
onAvatarLoadError(onAvatarLoadErrorParams);
|
||||
}
|
||||
}, [ onAvatarLoadError, onAvatarLoadErrorParams ]);
|
||||
|
||||
if (isIcon(url)) {
|
||||
return (
|
||||
<div
|
||||
className = { cx(_getAvatarClassName(), _getBadgeClassName()) }
|
||||
data-testid = { testId }
|
||||
id = { id }
|
||||
style = { _getAvatarStyle(color) }>
|
||||
<Icon
|
||||
size = '50%'
|
||||
src = { url } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (url) {
|
||||
return (
|
||||
<div className = { _getBadgeClassName() }>
|
||||
<img
|
||||
alt = 'avatar'
|
||||
className = { _getAvatarClassName() }
|
||||
crossOrigin = { useCORS ? '' : undefined }
|
||||
data-testid = { testId }
|
||||
id = { id }
|
||||
onError = { _onAvatarLoadError }
|
||||
src = { url }
|
||||
style = { _getAvatarStyle() } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (initials) {
|
||||
return (
|
||||
<div
|
||||
className = { cx(_getAvatarClassName(), _getBadgeClassName()) }
|
||||
data-testid = { testId }
|
||||
id = { id }
|
||||
style = { _getAvatarStyle(color) }>
|
||||
<div className = { classes.initialsContainer }>
|
||||
{initials}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// default avatar
|
||||
return (
|
||||
<div
|
||||
className = { cx(_getAvatarClassName('defaultAvatar'), _getBadgeClassName()) }
|
||||
data-testid = { testId }
|
||||
id = { id }
|
||||
style = { _getAvatarStyle() }>
|
||||
<Icon
|
||||
size = { '50%' }
|
||||
src = { iconUser } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default StatelessAvatar;
|
||||
4
react/features/base/avatar/constants.ts
Normal file
4
react/features/base/avatar/constants.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* The base URL for gravatar images.
|
||||
*/
|
||||
export const GRAVATAR_BASE_URL = 'https://www.gravatar.com/avatar/';
|
||||
95
react/features/base/avatar/functions.ts
Normal file
95
react/features/base/avatar/functions.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import GraphemeSplitter from 'grapheme-splitter';
|
||||
import { split } from 'lodash-es';
|
||||
|
||||
const AVATAR_COLORS = [
|
||||
'#6A50D3',
|
||||
'#FF9B42',
|
||||
'#DF486F',
|
||||
'#73348C',
|
||||
'#B23683',
|
||||
'#F96E57',
|
||||
'#4380E2',
|
||||
'#238561',
|
||||
'#00A8B3'
|
||||
];
|
||||
const wordSplitRegex = (/\s+|\.+|_+|;+|-+|,+|\|+|\/+|\\+|"+|'+|\(+|\)+|#+|&+/);
|
||||
const splitter = new GraphemeSplitter();
|
||||
|
||||
/**
|
||||
* Generates the background color of an initials based avatar.
|
||||
*
|
||||
* @param {string?} initials - The initials of the avatar.
|
||||
* @param {Array<string>} customAvatarBackgrounds - Custom avatar background values.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getAvatarColor(initials: string | undefined, customAvatarBackgrounds: Array<string>) {
|
||||
const hasCustomAvatarBackgronds = customAvatarBackgrounds?.length;
|
||||
const colorsBase = hasCustomAvatarBackgronds ? customAvatarBackgrounds : AVATAR_COLORS;
|
||||
|
||||
let colorIndex = 0;
|
||||
|
||||
if (initials) {
|
||||
let nameHash = 0;
|
||||
|
||||
for (const s of initials) {
|
||||
nameHash += Number(s.codePointAt(0));
|
||||
}
|
||||
|
||||
colorIndex = nameHash % colorsBase.length;
|
||||
}
|
||||
|
||||
return colorsBase[colorIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first grapheme from a word, uppercased.
|
||||
*
|
||||
* @param {string} word - The string to get grapheme from.
|
||||
* @returns {string}
|
||||
*/
|
||||
function getFirstGraphemeUpper(word: string) {
|
||||
if (!word?.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return splitter.splitGraphemes(word)[0].toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates initials for a simple string.
|
||||
*
|
||||
* @param {string?} s - The string to generate initials for.
|
||||
* @returns {string?}
|
||||
*/
|
||||
export function getInitials(s?: string) {
|
||||
// We don't want to use the domain part of an email address, if it is one
|
||||
const initialsBasis = split(s, '@')[0];
|
||||
const [ firstWord, ...remainingWords ] = initialsBasis.split(wordSplitRegex).filter(Boolean);
|
||||
|
||||
return getFirstGraphemeUpper(firstWord) + getFirstGraphemeUpper(remainingWords.pop() || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the passed URL should be loaded with CORS.
|
||||
*
|
||||
* @param {string | Function} url - The URL (on mobile we use a specific Icon component for avatars).
|
||||
* @param {Array<string>} corsURLs - The URL pattern that matches a URL that needs to be handled with CORS.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isCORSAvatarURL(url: string | Function, corsURLs: Array<string> = []): boolean {
|
||||
if (typeof url === 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return corsURLs.some(pattern => url.startsWith(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the passed prop is a loaded icon or not.
|
||||
*
|
||||
* @param {string? | Object?} iconProp - The prop to check.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isIcon(iconProp?: string | Function): iconProp is Function {
|
||||
return Boolean(iconProp) && (typeof iconProp === 'object' || typeof iconProp === 'function');
|
||||
}
|
||||
32
react/features/base/avatar/types.ts
Normal file
32
react/features/base/avatar/types.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export interface IAvatarProps {
|
||||
|
||||
/**
|
||||
* Color of the (initials based) avatar, if needed.
|
||||
*/
|
||||
color?: string;
|
||||
|
||||
/**
|
||||
* The user icon(browser only).
|
||||
*/
|
||||
iconUser?: any;
|
||||
|
||||
/**
|
||||
* Initials to be used to render the initials based avatars.
|
||||
*/
|
||||
initials?: string;
|
||||
|
||||
/**
|
||||
* Callback to signal the failure of the loading of the URL.
|
||||
*/
|
||||
onAvatarLoadError?: Function;
|
||||
|
||||
/**
|
||||
* Additional parameters to be passed to onAvatarLoadError function.
|
||||
*/
|
||||
onAvatarLoadErrorParams?: Object;
|
||||
|
||||
/**
|
||||
* Expected size of the avatar.
|
||||
*/
|
||||
size?: number;
|
||||
}
|
||||
244
react/features/base/buttons/CopyButton.web.tsx
Normal file
244
react/features/base/buttons/CopyButton.web.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Icon from '../icons/components/Icon';
|
||||
import { IconCheck, IconCopy } from '../icons/svg';
|
||||
import { copyText } from '../util/copyText.web';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
copyButton: {
|
||||
...theme.typography.bodyShortBold,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
padding: `${theme.spacing(2)} ${theme.spacing(3)}`,
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
background: theme.palette.action01,
|
||||
cursor: 'pointer',
|
||||
color: theme.palette.text01,
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.action01Hover
|
||||
},
|
||||
|
||||
'&.clicked': {
|
||||
background: theme.palette.success02
|
||||
},
|
||||
|
||||
'& > div > svg': {
|
||||
fill: theme.palette.icon01
|
||||
}
|
||||
},
|
||||
|
||||
content: {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap' as const,
|
||||
maxWidth: 292,
|
||||
marginRight: theme.spacing(3),
|
||||
|
||||
'&.selected': {
|
||||
fontWeight: 600
|
||||
}
|
||||
},
|
||||
|
||||
icon: {
|
||||
marginRight: theme.spacing(2)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
let mounted: boolean;
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The invisible text for screen readers.
|
||||
*
|
||||
* Intended to give the same info as `displayedText`, but can be customized to give more necessary context.
|
||||
* If not given, `displayedText` will be used.
|
||||
*/
|
||||
accessibilityText?: string;
|
||||
|
||||
/**
|
||||
* Css class to apply on container.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* The displayed text.
|
||||
*/
|
||||
displayedText: string;
|
||||
|
||||
/**
|
||||
* The id of the button.
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* The text displayed on copy success.
|
||||
*/
|
||||
textOnCopySuccess: string;
|
||||
|
||||
/**
|
||||
* The text displayed on mouse hover.
|
||||
*/
|
||||
textOnHover: string;
|
||||
|
||||
/**
|
||||
* The text that needs to be copied (might differ from the displayedText).
|
||||
*/
|
||||
textToCopy: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component meant to enable users to copy the conference URL.
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
function CopyButton({
|
||||
accessibilityText,
|
||||
className = '',
|
||||
displayedText,
|
||||
textToCopy,
|
||||
textOnHover,
|
||||
textOnCopySuccess,
|
||||
id
|
||||
}: IProps) {
|
||||
const { classes, cx } = useStyles();
|
||||
const [ isClicked, setIsClicked ] = useState(false);
|
||||
const [ isHovered, setIsHovered ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
mounted = true;
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Click handler for the element.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
async function onClick() {
|
||||
setIsHovered(false);
|
||||
|
||||
const isCopied = await copyText(textToCopy);
|
||||
|
||||
if (isCopied) {
|
||||
setIsClicked(true);
|
||||
|
||||
setTimeout(() => {
|
||||
// avoid: Can't perform a React state update on an unmounted component
|
||||
if (mounted) {
|
||||
setIsClicked(false);
|
||||
}
|
||||
}, 2500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hover handler for the element.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function onHoverIn() {
|
||||
if (!isClicked) {
|
||||
setIsHovered(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hover handler for the element.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function onHoverOut() {
|
||||
setIsHovered(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* KeyPress handler for accessibility.
|
||||
*
|
||||
* @param {React.KeyboardEventHandler<HTMLDivElement>} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function onKeyPress(e: React.KeyboardEvent) {
|
||||
if (onClick && (e.key === ' ' || e.key === 'Enter')) {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the content of the link based on the state.
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
function renderContent() {
|
||||
if (isClicked) {
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
className = { classes.icon }
|
||||
size = { 24 }
|
||||
src = { IconCheck } />
|
||||
<div className = { cx(classes.content, 'selected') }>
|
||||
<span role = { 'alert' }>{ textOnCopySuccess }</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
className = { classes.icon }
|
||||
size = { 24 }
|
||||
src = { IconCopy } />
|
||||
<div className = { classes.content }>
|
||||
<span> { isHovered ? textOnHover : displayedText } </span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
aria-describedby = { displayedText === textOnHover
|
||||
? undefined
|
||||
: `${id}-sr-text` }
|
||||
aria-label = { displayedText === textOnHover ? accessibilityText : textOnHover }
|
||||
className = { cx(className, classes.copyButton, isClicked ? ' clicked' : '') }
|
||||
id = { id }
|
||||
onBlur = { onHoverOut }
|
||||
onClick = { onClick }
|
||||
onFocus = { onHoverIn }
|
||||
onKeyPress = { onKeyPress }
|
||||
onMouseOut = { onHoverOut }
|
||||
onMouseOver = { onHoverIn }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
{ renderContent() }
|
||||
</div>
|
||||
|
||||
{ displayedText !== textOnHover && (
|
||||
<span
|
||||
className = 'sr-only'
|
||||
id = { `${id}-sr-text` }>
|
||||
{ accessibilityText }
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default CopyButton;
|
||||
162
react/features/base/color-scheme/ColorSchemeRegistry.ts
Normal file
162
react/features/base/color-scheme/ColorSchemeRegistry.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { IStateful } from '../app/types';
|
||||
import { toState } from '../redux/functions';
|
||||
import { StyleType } from '../styles/functions.any';
|
||||
|
||||
import defaultScheme from './defaultScheme';
|
||||
|
||||
/**
|
||||
* A registry class to register styles that need to be color-schemed.
|
||||
*
|
||||
* This class uses lazy initialization for scheme-ified style definitions on
|
||||
* request.
|
||||
*/
|
||||
class ColorSchemeRegistry {
|
||||
/**
|
||||
* A map of already scheme-ified style definitions.
|
||||
*/
|
||||
_schemedStyles = new Map();
|
||||
|
||||
/**
|
||||
* A map of registered style templates.
|
||||
*/
|
||||
_styleTemplates = new Map();
|
||||
|
||||
/**
|
||||
* Clears the already scheme-ified style definitions.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
clear() {
|
||||
this._schemedStyles.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the color-scheme applied style definition of a component.
|
||||
*
|
||||
* @param {Object | Function} stateful - An object or function that can be
|
||||
* resolved to Redux state using the {@code toState} function.
|
||||
* @param {string} componentName - The name of the component whose style we
|
||||
* want to retrieve.
|
||||
* @returns {StyleType}
|
||||
*/
|
||||
get(stateful: IStateful, componentName: string): StyleType {
|
||||
let schemedStyle = this._schemedStyles.get(componentName);
|
||||
|
||||
if (!schemedStyle) {
|
||||
schemedStyle
|
||||
= this._applyColorScheme(
|
||||
stateful,
|
||||
componentName,
|
||||
this._styleTemplates.get(componentName));
|
||||
this._schemedStyles.set(componentName, schemedStyle);
|
||||
}
|
||||
|
||||
return schemedStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a style definition to the registry for color-scheming.
|
||||
*
|
||||
* NOTE: It's suggested to only use this registry on styles where color
|
||||
* scheming is needed, otherwise just use a static style object as before.
|
||||
*
|
||||
* @param {string} componentName - The name of the component to register the
|
||||
* style to (e.g. {@code 'Toolbox'}).
|
||||
* @param {StyleType} style - The style definition to register.
|
||||
* @returns {void}
|
||||
*/
|
||||
register(componentName: string, style: any): void {
|
||||
this._styleTemplates.set(componentName, style);
|
||||
|
||||
// If this is a style overwrite, we need to delete the processed version
|
||||
// of the style from the other map
|
||||
this._schemedStyles.delete(componentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a color schemed style object applying the color scheme to every
|
||||
* colors in the style object prepared in a special way.
|
||||
*
|
||||
* @param {Object | Function} stateful - An object or function that can be
|
||||
* resolved to Redux state using the {@code toState} function.
|
||||
* @param {string} componentName - The name of the component to apply the
|
||||
* color scheme to.
|
||||
* @param {StyleType} style - The style definition to apply the color scheme
|
||||
* to.
|
||||
* @returns {StyleType}
|
||||
*/
|
||||
_applyColorScheme(
|
||||
stateful: IStateful,
|
||||
componentName: string,
|
||||
style: StyleType | null): StyleType {
|
||||
let schemedStyle: any;
|
||||
|
||||
if (Array.isArray(style)) {
|
||||
// The style is an array of styles, we apply the same transformation
|
||||
// to each, recursively.
|
||||
schemedStyle = [];
|
||||
|
||||
for (const entry of style) {
|
||||
schemedStyle.push(this._applyColorScheme(
|
||||
stateful, componentName, entry));
|
||||
}
|
||||
} else {
|
||||
// The style is an object, we create a copy of it to avoid in-place
|
||||
// modification.
|
||||
schemedStyle = {
|
||||
...style
|
||||
};
|
||||
|
||||
for (const [
|
||||
styleName,
|
||||
styleValue
|
||||
] of Object.entries(schemedStyle)) {
|
||||
if (typeof styleValue === 'object') {
|
||||
// The value is another style object, we apply the same
|
||||
// transformation recursively.
|
||||
schemedStyle[styleName]
|
||||
= this._applyColorScheme(
|
||||
stateful, componentName, styleValue as StyleType);
|
||||
} else if (typeof styleValue === 'function') {
|
||||
// The value is a function, which indicates that it's a
|
||||
// dynamic, schemed color we need to resolve.
|
||||
const value = styleValue();
|
||||
|
||||
schemedStyle[styleName]
|
||||
= this._getColor(stateful, componentName, value);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return schemedStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to get the color value for the provided identifier.
|
||||
*
|
||||
* @param {Object | Function} stateful - An object or function that can be
|
||||
* resolved to Redux state using the {@code toState} function.
|
||||
* @param {string} componentName - The name of the component to get the
|
||||
* color value for.
|
||||
* @param {string} colorDefinition - The string identifier of the color,
|
||||
* e.g. {@code appBackground}.
|
||||
* @returns {string}
|
||||
*/
|
||||
_getColor(
|
||||
stateful: IStateful,
|
||||
componentName: string,
|
||||
colorDefinition: string): string {
|
||||
const colorScheme = toState(stateful)['features/base/color-scheme'] || {};
|
||||
|
||||
return {
|
||||
...defaultScheme._defaultTheme,
|
||||
...colorScheme._defaultTheme,
|
||||
...defaultScheme[componentName as keyof typeof defaultScheme],
|
||||
...colorScheme[componentName]
|
||||
}[colorDefinition];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new ColorSchemeRegistry();
|
||||
30
react/features/base/color-scheme/defaultScheme.ts
Normal file
30
react/features/base/color-scheme/defaultScheme.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ColorPalette } from '../styles/components/styles/ColorPalette';
|
||||
import { getRGBAFormat } from '../styles/functions.any';
|
||||
|
||||
/**
|
||||
* The default color scheme of the application.
|
||||
*/
|
||||
export default {
|
||||
'_defaultTheme': {
|
||||
// Generic app theme colors that are used across the entire app.
|
||||
// All scheme definitions below inherit these values.
|
||||
background: 'rgb(255, 255, 255)',
|
||||
errorText: ColorPalette.red,
|
||||
icon: 'rgb(28, 32, 37)',
|
||||
text: 'rgb(28, 32, 37)'
|
||||
},
|
||||
'Dialog': {},
|
||||
'Header': {
|
||||
background: ColorPalette.blue,
|
||||
icon: ColorPalette.white,
|
||||
statusBar: ColorPalette.blueHighlight,
|
||||
statusBarContent: ColorPalette.white,
|
||||
text: ColorPalette.white
|
||||
},
|
||||
'Toolbox': {
|
||||
button: 'rgb(255, 255, 255)',
|
||||
buttonToggled: 'rgb(38, 58, 76)',
|
||||
buttonToggledBorder: getRGBAFormat('#a4b8d1', 0.6),
|
||||
hangup: 'rgb(227,79,86)'
|
||||
}
|
||||
};
|
||||
11
react/features/base/color-scheme/functions.ts
Normal file
11
react/features/base/color-scheme/functions.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* A special function to be used in the {@code createColorSchemedStyle} call,
|
||||
* that denotes that the color is a dynamic color.
|
||||
*
|
||||
* @param {string} colorDefinition - The definition of the color to mark to be
|
||||
* resolved.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function schemeColor(colorDefinition: string): Function {
|
||||
return () => colorDefinition;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"panePadding": 24,
|
||||
"participantsPaneWidth": 315,
|
||||
"MD_BREAKPOINT": "580px"
|
||||
}
|
||||
381
react/features/base/conference/actionTypes.ts
Normal file
381
react/features/base/conference/actionTypes.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
/**
|
||||
* The type of (redux) action which signals that server authentication has
|
||||
* becoming available or unavailable or logged in user has changed.
|
||||
*
|
||||
* {
|
||||
* type: AUTH_STATUS_CHANGED,
|
||||
* authEnabled: boolean,
|
||||
* authLogin: string
|
||||
* }
|
||||
*/
|
||||
export const AUTH_STATUS_CHANGED = 'AUTH_STATUS_CHANGED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that a specific conference failed.
|
||||
*
|
||||
* {
|
||||
* type: CONFERENCE_FAILED,
|
||||
* conference: JitsiConference,
|
||||
* error: Error
|
||||
* }
|
||||
*/
|
||||
export const CONFERENCE_FAILED = 'CONFERENCE_FAILED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that a specific conference was
|
||||
* joined.
|
||||
*
|
||||
* {
|
||||
* type: CONFERENCE_JOINED,
|
||||
* conference: JitsiConference
|
||||
* }
|
||||
*/
|
||||
export const CONFERENCE_JOINED = 'CONFERENCE_JOINED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that a specific conference joining is in progress.
|
||||
* A CONFERENCE_JOINED is guaranteed to follow.
|
||||
*
|
||||
* {
|
||||
* type: CONFERENCE_JOIN_IN_PROGRESS,
|
||||
* conference: JitsiConference
|
||||
* }
|
||||
*/
|
||||
export const CONFERENCE_JOIN_IN_PROGRESS = 'CONFERENCE_JOIN_IN_PROGRESS';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that a specific conference was left.
|
||||
*
|
||||
* {
|
||||
* type: CONFERENCE_LEFT,
|
||||
* conference: JitsiConference
|
||||
* }
|
||||
*/
|
||||
export const CONFERENCE_LEFT = 'CONFERENCE_LEFT';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that the conference is out of focus.
|
||||
* For example, if the user navigates to the Chat screen.
|
||||
*
|
||||
* {
|
||||
* type: CONFERENCE_BLURRED,
|
||||
* }
|
||||
*/
|
||||
export const CONFERENCE_BLURRED = 'CONFERENCE_BLURRED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that the conference is in focus.
|
||||
*
|
||||
* {
|
||||
* type: CONFERENCE_FOCUSED,
|
||||
* }
|
||||
*/
|
||||
export const CONFERENCE_FOCUSED = 'CONFERENCE_FOCUSED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action, which indicates conference local subject changes.
|
||||
*
|
||||
* {
|
||||
* type: CONFERENCE_LOCAL_SUBJECT_CHANGED
|
||||
* subject: string
|
||||
* }
|
||||
*/
|
||||
export const CONFERENCE_LOCAL_SUBJECT_CHANGED = 'CONFERENCE_LOCAL_SUBJECT_CHANGED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action, which indicates conference properties change.
|
||||
*
|
||||
* {
|
||||
* type: CONFERENCE_PROPERTIES_CHANGED
|
||||
* properties: {
|
||||
* audio-recording-enabled: boolean,
|
||||
* visitor-count: number
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export const CONFERENCE_PROPERTIES_CHANGED = 'CONFERENCE_PROPERTIES_CHANGED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action, which indicates conference subject changes.
|
||||
*
|
||||
* {
|
||||
* type: CONFERENCE_SUBJECT_CHANGED
|
||||
* subject: string
|
||||
* }
|
||||
*/
|
||||
export const CONFERENCE_SUBJECT_CHANGED = 'CONFERENCE_SUBJECT_CHANGED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action, which indicates conference UTC timestamp changes.
|
||||
*
|
||||
* {
|
||||
* type: CONFERENCE_TIMESTAMP_CHANGED
|
||||
* timestamp: number
|
||||
* }
|
||||
*/
|
||||
export const CONFERENCE_TIMESTAMP_CHANGED = 'CONFERENCE_TIMESTAMP_CHANGED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that an uuid for a conference has been set.
|
||||
*
|
||||
* {
|
||||
* type: CONFERENCE_UNIQUE_ID_SET,
|
||||
* conference: JitsiConference
|
||||
* }
|
||||
*/
|
||||
export const CONFERENCE_UNIQUE_ID_SET = 'CONFERENCE_UNIQUE_ID_SET';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that the end-to-end RTT against a specific remote participant has changed.
|
||||
*
|
||||
* {
|
||||
* type: E2E_RTT_CHANGED,
|
||||
* e2eRtt: {
|
||||
* rtt: number,
|
||||
* participant: Object,
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export const E2E_RTT_CHANGED = 'E2E_RTT_CHANGED'
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that a conference will be initialized.
|
||||
*
|
||||
* {
|
||||
* type: CONFERENCE_WILL_INIT
|
||||
* }
|
||||
*/
|
||||
export const CONFERENCE_WILL_INIT = 'CONFERENCE_WILL_INIT';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that a specific conference will be
|
||||
* joined.
|
||||
*
|
||||
* {
|
||||
* type: CONFERENCE_WILL_JOIN,
|
||||
* conference: JitsiConference
|
||||
* }
|
||||
*/
|
||||
export const CONFERENCE_WILL_JOIN = 'CONFERENCE_WILL_JOIN';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that a specific conference will be
|
||||
* left.
|
||||
*
|
||||
* {
|
||||
* type: CONFERENCE_WILL_LEAVE,
|
||||
* conference: JitsiConference
|
||||
* }
|
||||
*/
|
||||
export const CONFERENCE_WILL_LEAVE = 'CONFERENCE_WILL_LEAVE';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that the data channel with the
|
||||
* bridge has been established.
|
||||
*
|
||||
* {
|
||||
* type: DATA_CHANNEL_OPENED
|
||||
* }
|
||||
*/
|
||||
export const DATA_CHANNEL_OPENED = 'DATA_CHANNEL_OPENED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that the data channel with the
|
||||
* bridge has been closed.
|
||||
*
|
||||
* {
|
||||
* type: DATA_CHANNEL_CLOSED,
|
||||
* code: number,
|
||||
* reason: string
|
||||
* }
|
||||
*/
|
||||
export const DATA_CHANNEL_CLOSED = 'DATA_CHANNEL_CLOSED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which indicates that an endpoint message
|
||||
* sent by another participant to the data channel is received.
|
||||
*
|
||||
* {
|
||||
* type: ENDPOINT_MESSAGE_RECEIVED,
|
||||
* participant: Object,
|
||||
* data: Object
|
||||
* }
|
||||
*/
|
||||
export const ENDPOINT_MESSAGE_RECEIVED = 'ENDPOINT_MESSAGE_RECEIVED';
|
||||
|
||||
/**
|
||||
* The type of action which signals that the user has been kicked out from
|
||||
* the conference.
|
||||
*
|
||||
* {
|
||||
* type: KICKED_OUT,
|
||||
* conference: JitsiConference
|
||||
* }
|
||||
*/
|
||||
export const KICKED_OUT = 'KICKED_OUT';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that the lock state of a specific
|
||||
* {@code JitsiConference} changed.
|
||||
*
|
||||
* {
|
||||
* type: LOCK_STATE_CHANGED,
|
||||
* conference: JitsiConference,
|
||||
* locked: boolean
|
||||
* }
|
||||
*/
|
||||
export const LOCK_STATE_CHANGED = 'LOCK_STATE_CHANGED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that a system (non-participant) message has been received.
|
||||
*
|
||||
* {
|
||||
* type: NON_PARTICIPANT_MESSAGE_RECEIVED,
|
||||
* id: String,
|
||||
* json: Object
|
||||
* }
|
||||
*/
|
||||
export const NON_PARTICIPANT_MESSAGE_RECEIVED = 'NON_PARTICIPANT_MESSAGE_RECEIVED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the peer2peer flag for the current
|
||||
* conference.
|
||||
*
|
||||
* {
|
||||
* type: P2P_STATUS_CHANGED,
|
||||
* p2p: boolean
|
||||
* }
|
||||
*/
|
||||
export const P2P_STATUS_CHANGED = 'P2P_STATUS_CHANGED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals to play specified touch tones.
|
||||
*
|
||||
* {
|
||||
* type: SEND_TONES,
|
||||
* tones: string,
|
||||
* duration: number,
|
||||
* pause: number
|
||||
* }
|
||||
*/
|
||||
export const SEND_TONES = 'SEND_TONES';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which updates the current known status of the
|
||||
* Follow Me feature.
|
||||
*
|
||||
* {
|
||||
* type: SET_FOLLOW_ME,
|
||||
* enabled: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_FOLLOW_ME = 'SET_FOLLOW_ME';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which updates the current known status of the
|
||||
* Follow Me feature that is used only by the recorder.
|
||||
*
|
||||
* {
|
||||
* type: SET_FOLLOW_ME_RECORDER,
|
||||
* enabled: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_FOLLOW_ME_RECORDER = 'SET_FOLLOW_ME_RECORDER';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the obfuscated room name.
|
||||
*
|
||||
* {
|
||||
* type: SET_OBFUSCATED_ROOM,
|
||||
* obfuscatedRoom: string
|
||||
* }
|
||||
*/
|
||||
export const SET_OBFUSCATED_ROOM = 'SET_OBFUSCATED_ROOM';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which updates the current known status of the
|
||||
* Mute Reactions Sound feature.
|
||||
*
|
||||
* {
|
||||
* type: SET_START_REACTIONS_MUTED,
|
||||
* enabled: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_START_REACTIONS_MUTED = 'SET_START_REACTIONS_MUTED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the password to join or lock a specific
|
||||
* {@code JitsiConference}.
|
||||
*
|
||||
* {
|
||||
* type: SET_PASSWORD,
|
||||
* conference: JitsiConference,
|
||||
* method: Function
|
||||
* password: string
|
||||
* }
|
||||
*/
|
||||
export const SET_PASSWORD = 'SET_PASSWORD';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that setting a password on a
|
||||
* {@code JitsiConference} failed (with an error).
|
||||
*
|
||||
* {
|
||||
* type: SET_PASSWORD_FAILED,
|
||||
* error: string
|
||||
* }
|
||||
*/
|
||||
export const SET_PASSWORD_FAILED = 'SET_PASSWORD_FAILED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals for pending subject changes.
|
||||
*
|
||||
* {
|
||||
* type: SET_PENDING_SUBJECT_CHANGE,
|
||||
* subject: string
|
||||
* }
|
||||
*/
|
||||
export const SET_PENDING_SUBJECT_CHANGE = 'SET_PENDING_SUBJECT_CHANGE';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the name of the room of the
|
||||
* conference to be joined.
|
||||
*
|
||||
* {
|
||||
* type: SET_ROOM,
|
||||
* room: string
|
||||
* }
|
||||
*/
|
||||
export const SET_ROOM = 'SET_ROOM';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which updates the current known status of the
|
||||
* moderator features for starting participants as audio or video muted.
|
||||
*
|
||||
* {
|
||||
* type: SET_START_MUTED_POLICY,
|
||||
* startAudioMutedPolicy: boolean,
|
||||
* startVideoMutedPolicy: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_START_MUTED_POLICY = 'SET_START_MUTED_POLICY';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which updates the assumed bandwidth bps.
|
||||
*
|
||||
* {
|
||||
* type: SET_ASSUMED_BANDWIDTH_BPS,
|
||||
* assumedBandwidthBps: number
|
||||
* }
|
||||
*/
|
||||
export const SET_ASSUMED_BANDWIDTH_BPS = 'SET_ASSUMED_BANDWIDTH_BPS';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which updated the conference metadata.
|
||||
*
|
||||
* {
|
||||
* type: UPDATE_CONFERENCE_METADATA,
|
||||
* metadata: Object
|
||||
* }
|
||||
*/
|
||||
export const UPDATE_CONFERENCE_METADATA = 'UPDATE_CONFERENCE_METADATA';
|
||||
1152
react/features/base/conference/actions.any.ts
Normal file
1152
react/features/base/conference/actions.any.ts
Normal file
File diff suppressed because it is too large
Load Diff
29
react/features/base/conference/actions.native.ts
Normal file
29
react/features/base/conference/actions.native.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { IStore } from '../../app/types';
|
||||
import { setAudioMuted, setVideoMuted } from '../media/actions';
|
||||
import { MEDIA_TYPE, MediaType, VIDEO_MUTISM_AUTHORITY } from '../media/constants';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
* Starts audio and/or video for the visitor.
|
||||
*
|
||||
* @param {Array<MediaType>} mediaTypes - The media types that need to be started.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setupVisitorStartupMedia(mediaTypes: Array<MediaType>) {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
if (!mediaTypes || !Array.isArray(mediaTypes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mediaTypes.forEach(mediaType => {
|
||||
switch (mediaType) {
|
||||
case MEDIA_TYPE.AUDIO:
|
||||
dispatch(setAudioMuted(false, true));
|
||||
break;
|
||||
case MEDIA_TYPE.VIDEO:
|
||||
dispatch(setVideoMuted(false, VIDEO_MUTISM_AUTHORITY.USER, true));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
25
react/features/base/conference/actions.web.ts
Normal file
25
react/features/base/conference/actions.web.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { IStore } from '../../app/types';
|
||||
import { gumPending } from '../media/actions';
|
||||
import { MEDIA_TYPE, MediaType } from '../media/constants';
|
||||
import { IGUMPendingState } from '../media/types';
|
||||
import { createAndAddInitialAVTracks } from '../tracks/actions.web';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
* Starts audio and/or video for the visitor.
|
||||
*
|
||||
* @param {Array<MediaType>} media - The media types that need to be started.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setupVisitorStartupMedia(media: Array<MediaType>) {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
// Clear the gum pending state in case we have set it to pending since we are starting the
|
||||
// conference without tracks.
|
||||
dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
|
||||
|
||||
if (media && Array.isArray(media) && media.length > 0) {
|
||||
dispatch(createAndAddInitialAVTracks(media));
|
||||
}
|
||||
};
|
||||
}
|
||||
47
react/features/base/conference/constants.ts
Normal file
47
react/features/base/conference/constants.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* The command type for updating a participant's avatar URL.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const AVATAR_URL_COMMAND = 'avatar-url';
|
||||
|
||||
/**
|
||||
* The command type for updating a participant's email address.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const EMAIL_COMMAND = 'email';
|
||||
|
||||
/**
|
||||
* The name of the {@code JitsiConference} property which identifies the URL of
|
||||
* the conference represented by the {@code JitsiConference} instance.
|
||||
*
|
||||
* TODO It was introduced in a moment of desperation. Jitsi Meet SDK for Android
|
||||
* and iOS needs to deliver events from the JavaScript side where they originate
|
||||
* to the Java and Objective-C sides, respectively, where they are to be
|
||||
* handled. The URL of the {@code JitsiConference} was chosen as the identifier
|
||||
* because the Java and Objective-C sides join by URL through their respective
|
||||
* loadURL methods. But features/base/connection's {@code locationURL} is not
|
||||
* guaranteed at the time of this writing to match the {@code JitsiConference}
|
||||
* instance when the events are to be fired. Patching {@code JitsiConference}
|
||||
* from the outside is not cool but it should suffice for now.
|
||||
*/
|
||||
export const JITSI_CONFERENCE_URL_KEY = Symbol('url');
|
||||
|
||||
export const TRIGGER_READY_TO_CLOSE_REASONS = {
|
||||
'dialog.sessTerminatedReason': 'The meeting has been terminated',
|
||||
'lobby.lobbyClosed': 'Lobby room closed.'
|
||||
};
|
||||
|
||||
/**
|
||||
* Conference leave reasons.
|
||||
*/
|
||||
export const CONFERENCE_LEAVE_REASONS = {
|
||||
SWITCH_ROOM: 'switch_room',
|
||||
UNRECOVERABLE_ERROR: 'unrecoverable_error'
|
||||
};
|
||||
|
||||
/**
|
||||
* The ID of the notification that is shown when the user is muted by focus.
|
||||
*/
|
||||
export const START_MUTED_NOTIFICATION_ID = 'start-muted';
|
||||
620
react/features/base/conference/functions.ts
Normal file
620
react/features/base/conference/functions.ts
Normal file
@@ -0,0 +1,620 @@
|
||||
import { sha512_256 as sha512 } from 'js-sha512';
|
||||
import { upperFirst, words } from 'lodash-es';
|
||||
|
||||
import { getName } from '../../app/functions';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { determineTranscriptionLanguage } from '../../transcribing/functions';
|
||||
import { IStateful } from '../app/types';
|
||||
import { JitsiTrackErrors } from '../lib-jitsi-meet';
|
||||
import { setAudioMuted, setVideoMuted } from '../media/actions';
|
||||
import { VIDEO_MUTISM_AUTHORITY } from '../media/constants';
|
||||
import {
|
||||
participantJoined,
|
||||
participantLeft
|
||||
} from '../participants/actions';
|
||||
import { getLocalParticipant } from '../participants/functions';
|
||||
import { toState } from '../redux/functions';
|
||||
import {
|
||||
appendURLParam,
|
||||
getBackendSafePath,
|
||||
safeDecodeURIComponent
|
||||
} from '../util/uri';
|
||||
|
||||
import { setObfuscatedRoom } from './actions';
|
||||
import {
|
||||
AVATAR_URL_COMMAND,
|
||||
EMAIL_COMMAND,
|
||||
JITSI_CONFERENCE_URL_KEY,
|
||||
START_MUTED_NOTIFICATION_ID
|
||||
} from './constants';
|
||||
import logger from './logger';
|
||||
import { IJitsiConference } from './reducer';
|
||||
|
||||
/**
|
||||
* Returns root conference state.
|
||||
*
|
||||
* @param {IReduxState} state - Global state.
|
||||
* @returns {Object} Conference state.
|
||||
*/
|
||||
export const getConferenceState = (state: IReduxState) => state['features/base/conference'];
|
||||
|
||||
/**
|
||||
* Attach a set of local tracks to a conference.
|
||||
*
|
||||
* @param {JitsiConference} conference - Conference instance.
|
||||
* @param {JitsiLocalTrack[]} localTracks - List of local media tracks.
|
||||
* @protected
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function _addLocalTracksToConference(
|
||||
conference: IJitsiConference,
|
||||
localTracks: Array<Object>) {
|
||||
const conferenceLocalTracks = conference.getLocalTracks();
|
||||
const promises = [];
|
||||
|
||||
for (const track of localTracks) {
|
||||
// XXX The library lib-jitsi-meet may be draconian, for example, when
|
||||
// adding one and the same video track multiple times.
|
||||
if (conferenceLocalTracks.indexOf(track) === -1) {
|
||||
promises.push(
|
||||
conference.addTrack(track).catch((err: Error) => {
|
||||
_reportError(
|
||||
'Failed to add local track to conference',
|
||||
err);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logic shared between web and RN which processes the {@code USER_JOINED}
|
||||
* conference event and dispatches either {@link participantJoined} or
|
||||
* {@link hiddenParticipantJoined}.
|
||||
*
|
||||
* @param {Object} store - The redux store.
|
||||
* @param {JitsiMeetConference} conference - The conference for which the
|
||||
* {@code USER_JOINED} event is being processed.
|
||||
* @param {JitsiParticipant} user - The user who has just joined.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function commonUserJoinedHandling(
|
||||
{ dispatch }: { dispatch: IStore['dispatch']; },
|
||||
conference: IJitsiConference,
|
||||
user: any) {
|
||||
const id = user.getId();
|
||||
const displayName = user.getDisplayName();
|
||||
|
||||
if (!user.isHidden()) {
|
||||
const isReplacing = user?.isReplacing();
|
||||
const isPromoted = conference?.getMetadataHandler().getMetadata()?.visitors?.promoted?.[id];
|
||||
|
||||
// the identity and avatar come from jwt and never change in the presence
|
||||
dispatch(participantJoined({
|
||||
avatarURL: user.getIdentity()?.user?.avatar,
|
||||
botType: user.getBotType(),
|
||||
conference,
|
||||
id,
|
||||
name: displayName,
|
||||
presence: user.getStatus(),
|
||||
role: user.getRole(),
|
||||
isPromoted,
|
||||
isReplacing,
|
||||
sources: user.getSources()
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logic shared between web and RN which processes the {@code USER_LEFT}
|
||||
* conference event and dispatches either {@link participantLeft} or
|
||||
* {@link hiddenParticipantLeft}.
|
||||
*
|
||||
* @param {Object} store - The redux store.
|
||||
* @param {JitsiMeetConference} conference - The conference for which the
|
||||
* {@code USER_LEFT} event is being processed.
|
||||
* @param {JitsiParticipant} user - The user who has just left.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function commonUserLeftHandling(
|
||||
{ dispatch }: { dispatch: IStore['dispatch']; },
|
||||
conference: IJitsiConference,
|
||||
user: any) {
|
||||
const id = user.getId();
|
||||
|
||||
if (!user.isHidden()) {
|
||||
const isReplaced = user.isReplaced?.();
|
||||
|
||||
dispatch(participantLeft(id, conference, { isReplaced }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a specific predicate for each {@link JitsiConference} known to the
|
||||
* redux state features/base/conference while it returns {@code true}.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store, state, or
|
||||
* {@code getState} function.
|
||||
* @param {Function} predicate - The predicate to evaluate for each
|
||||
* {@code JitsiConference} know to the redux state features/base/conference
|
||||
* while it returns {@code true}.
|
||||
* @returns {boolean} If the specified {@code predicate} returned {@code true}
|
||||
* for all {@code JitsiConference} instances known to the redux state
|
||||
* features/base/conference.
|
||||
*/
|
||||
export function forEachConference(
|
||||
stateful: IStateful,
|
||||
predicate: (a: any, b: URL) => boolean) {
|
||||
const state = getConferenceState(toState(stateful));
|
||||
|
||||
for (const v of Object.values(state)) {
|
||||
// Does the value of the base/conference's property look like a
|
||||
// JitsiConference?
|
||||
if (v && typeof v === 'object') {
|
||||
const url: URL = v[JITSI_CONFERENCE_URL_KEY];
|
||||
|
||||
// XXX The Web version of Jitsi Meet does not utilize
|
||||
// JITSI_CONFERENCE_URL_KEY at the time of this writing. An
|
||||
// alternative is necessary then to recognize JitsiConference
|
||||
// instances and myUserId is as good as any other property.
|
||||
if ((url || typeof v.myUserId === 'function')
|
||||
&& !predicate(v, url)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the display name of the conference.
|
||||
*
|
||||
* @param {IStateful} stateful - Reference that can be resolved to Redux
|
||||
* state with the {@code toState} function.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getConferenceName(stateful: IStateful): string {
|
||||
const state = toState(stateful);
|
||||
const { callee } = state['features/base/jwt'];
|
||||
const { callDisplayName } = state['features/base/config'];
|
||||
const { localSubject, pendingSubjectChange, room, subject } = getConferenceState(state);
|
||||
|
||||
return (localSubject
|
||||
|| pendingSubjectChange
|
||||
|| subject
|
||||
|| callDisplayName
|
||||
|| callee?.name
|
||||
|| (room && safeStartCase(safeDecodeURIComponent(room)))) ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the conference formatted for the title.
|
||||
*
|
||||
* @param {IStateful} stateful - Reference that can be resolved to Redux state with the {@code toState}
|
||||
* function.
|
||||
* @returns {string} - The name of the conference formatted for the title.
|
||||
*/
|
||||
export function getConferenceNameForTitle(stateful: IStateful) {
|
||||
return safeStartCase(safeDecodeURIComponent(getConferenceState(toState(stateful)).room ?? ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object aggregating the conference options.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store state.
|
||||
* @returns {Object} - Options object.
|
||||
*/
|
||||
export function getConferenceOptions(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
|
||||
const config = state['features/base/config'];
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
const { defaultTranscriptionLanguage } = state['features/dynamic-branding'];
|
||||
const { tenant } = state['features/base/jwt'];
|
||||
const { email, name: nick } = getLocalParticipant(state) ?? {};
|
||||
const options: any = { ...config };
|
||||
|
||||
if (tenant) {
|
||||
options.siteID = tenant;
|
||||
}
|
||||
|
||||
if (options.enableDisplayNameInStats && nick) {
|
||||
options.statisticsDisplayName = nick;
|
||||
}
|
||||
|
||||
if (options.enableEmailInStats && email) {
|
||||
options.statisticsId = email;
|
||||
}
|
||||
|
||||
if (locationURL) {
|
||||
options.confID = `${locationURL.host}${getBackendSafePath(locationURL.pathname)}`;
|
||||
}
|
||||
|
||||
options.applicationName = getName();
|
||||
options.transcriptionLanguage
|
||||
= defaultTranscriptionLanguage ?? determineTranscriptionLanguage(options);
|
||||
|
||||
// Disable analytics, if requested.
|
||||
if (options.disableThirdPartyRequests) {
|
||||
delete config.analytics?.scriptURLs;
|
||||
delete config.analytics?.amplitudeAPPKey;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the restored conference options if anything is available to be restored or undefined.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store state.
|
||||
* @returns {Object?}
|
||||
*/
|
||||
export function restoreConferenceOptions(stateful: IStateful) {
|
||||
const config = toState(stateful)['features/base/config'];
|
||||
|
||||
if (config.oldConfig) {
|
||||
return {
|
||||
hosts: {
|
||||
domain: config.oldConfig.hosts.domain,
|
||||
muc: config.oldConfig.hosts.muc
|
||||
},
|
||||
focusUserJid: config.oldConfig.focusUserJid,
|
||||
disableFocus: false,
|
||||
bosh: config.oldConfig.bosh,
|
||||
websocket: config.oldConfig.websocket,
|
||||
oldConfig: undefined
|
||||
};
|
||||
}
|
||||
|
||||
// nothing to return
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the global config (that is, window.config) with XMPP configuration required to join as a visitor.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store state.
|
||||
* @param {string|undefined} vnode - The received parameters.
|
||||
* @param {string} focusJid - The received parameters.
|
||||
* @param {string|undefined} username - The received parameters.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getVisitorOptions(stateful: IStateful, vnode: string, focusJid: string, username: string) {
|
||||
const config = toState(stateful)['features/base/config'];
|
||||
|
||||
if (!config?.hosts) {
|
||||
logger.warn('Wrong configuration, missing hosts.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!vnode) {
|
||||
// this is redirecting back to main, lets restore config
|
||||
// not updating disableFocus, as if the room capacity is full the promotion to the main room will fail
|
||||
// and the visitor will be redirected back to a vnode from jicofo
|
||||
if (config.oldConfig && username) {
|
||||
return {
|
||||
hosts: config.oldConfig.hosts,
|
||||
focusUserJid: focusJid,
|
||||
disableLocalStatsBroadcast: false,
|
||||
bosh: config.oldConfig.bosh && appendURLParam(config.oldConfig.bosh, 'customusername', username),
|
||||
p2p: config.oldConfig.p2p,
|
||||
websocket: config.oldConfig.websocket
|
||||
&& appendURLParam(config.oldConfig.websocket, 'customusername', username),
|
||||
oldConfig: undefined // clears it up
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const oldConfig = {
|
||||
hosts: {
|
||||
domain: ''
|
||||
},
|
||||
focusUserJid: config.focusUserJid,
|
||||
bosh: config.bosh,
|
||||
p2p: config.p2p,
|
||||
websocket: config.websocket
|
||||
};
|
||||
|
||||
// copy original hosts, to make sure we do not use a modified one later
|
||||
Object.assign(oldConfig.hosts, config.hosts);
|
||||
|
||||
const domain = `${vnode}.meet.jitsi`;
|
||||
|
||||
return {
|
||||
oldConfig,
|
||||
hosts: {
|
||||
domain,
|
||||
muc: config.hosts.muc.replace(oldConfig.hosts.domain, domain)
|
||||
},
|
||||
focusUserJid: focusJid,
|
||||
disableFocus: true, // This flag disables sending the initial conference request
|
||||
disableLocalStatsBroadcast: true,
|
||||
bosh: config.bosh && appendURLParam(config.bosh, 'vnode', vnode),
|
||||
p2p: {
|
||||
...config.p2p,
|
||||
enabled: false
|
||||
},
|
||||
websocket: config.websocket && appendURLParam(config.websocket, 'vnode', vnode)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the UTC timestamp when the first participant joined the conference.
|
||||
*
|
||||
* @param {IStateful} stateful - Reference that can be resolved to Redux
|
||||
* state with the {@code toState} function.
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getConferenceTimestamp(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const { conferenceTimestamp } = getConferenceState(state);
|
||||
|
||||
return conferenceTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current {@code JitsiConference} which is joining or joined and is
|
||||
* not leaving. Please note the contrast with merely reading the
|
||||
* {@code conference} state of the feature base/conference which is not joining
|
||||
* but may be leaving already.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store, state, or
|
||||
* {@code getState} function.
|
||||
* @returns {JitsiConference|undefined}
|
||||
*/
|
||||
export function getCurrentConference(stateful: IStateful): IJitsiConference | undefined {
|
||||
const { conference, joining, leaving, membersOnly, passwordRequired }
|
||||
= getConferenceState(toState(stateful));
|
||||
|
||||
// There is a precedence
|
||||
if (conference) {
|
||||
return conference === leaving ? undefined : conference;
|
||||
}
|
||||
|
||||
return joining || passwordRequired || membersOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current conference is a P2P connection.
|
||||
* Will return `false` if it's a JVB one, and `null` if there is no conference.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store, state, or
|
||||
* {@code getState} function.
|
||||
* @returns {boolean|null}
|
||||
*/
|
||||
export function isP2pActive(stateful: IStateful): boolean | null {
|
||||
const conference = getCurrentConference(toState(stateful));
|
||||
|
||||
if (!conference) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return conference.isP2PActive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored room name.
|
||||
*
|
||||
* @param {IReduxState} state - The current state of the app.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getRoomName(state: IReduxState) {
|
||||
return getConferenceState(state).room;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an obfuscated room name or create and persist it if it doesn't exists.
|
||||
*
|
||||
* @param {IReduxState} state - The current state of the app.
|
||||
* @param {Function} dispatch - The Redux dispatch function.
|
||||
* @returns {string} - Obfuscated room name.
|
||||
*/
|
||||
export function getOrCreateObfuscatedRoomName(state: IReduxState, dispatch: IStore['dispatch']) {
|
||||
let { obfuscatedRoom } = getConferenceState(state);
|
||||
const { obfuscatedRoomSource } = getConferenceState(state);
|
||||
const room = getRoomName(state);
|
||||
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
|
||||
// On native mobile the store doesn't clear when joining a new conference so we might have the obfuscatedRoom
|
||||
// stored even though a different room was joined.
|
||||
// Check if the obfuscatedRoom was already computed for the current room.
|
||||
if (!obfuscatedRoom || (obfuscatedRoomSource !== room)) {
|
||||
obfuscatedRoom = sha512(room);
|
||||
dispatch(setObfuscatedRoom(obfuscatedRoom, room));
|
||||
}
|
||||
|
||||
return obfuscatedRoom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analytics may require an obfuscated room name, this functions decides based on a config if the normal or
|
||||
* obfuscated room name should be returned.
|
||||
*
|
||||
* @param {IReduxState} state - The current state of the app.
|
||||
* @param {Function} dispatch - The Redux dispatch function.
|
||||
* @returns {string} - Analytics room name.
|
||||
*/
|
||||
export function getAnalyticsRoomName(state: IReduxState, dispatch: IStore['dispatch']) {
|
||||
const { analysis: { obfuscateRoomName = false } = {} } = state['features/base/config'];
|
||||
|
||||
if (obfuscateRoomName) {
|
||||
return getOrCreateObfuscatedRoomName(state, dispatch);
|
||||
}
|
||||
|
||||
return getRoomName(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an error thrown by the backend (i.e. {@code lib-jitsi-meet}) while
|
||||
* manipulating a conference participant (e.g. Pin or select participant).
|
||||
*
|
||||
* @param {Error} err - The Error which was thrown by the backend while
|
||||
* manipulating a conference participant and which is to be handled.
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
export function _handleParticipantError(err: Error) {
|
||||
// XXX DataChannels are initialized at some later point when the conference
|
||||
// has multiple participants, but code that pins or selects a participant
|
||||
// might be executed before. So here we're swallowing a particular error.
|
||||
// TODO Lib-jitsi-meet should be fixed to not throw such an exception in
|
||||
// these scenarios.
|
||||
if (err.message !== 'Data channels support is disabled!') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a specific string is a valid room name.
|
||||
*
|
||||
* @param {(string|undefined)} room - The name of the conference room to check
|
||||
* for validity.
|
||||
* @returns {boolean} If the specified room name is valid, then true; otherwise,
|
||||
* false.
|
||||
*/
|
||||
export function isRoomValid(room?: string) {
|
||||
return typeof room === 'string' && room !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a set of local tracks from a conference.
|
||||
*
|
||||
* @param {JitsiConference} conference - Conference instance.
|
||||
* @param {JitsiLocalTrack[]} localTracks - List of local media tracks.
|
||||
* @protected
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function _removeLocalTracksFromConference(
|
||||
conference: IJitsiConference,
|
||||
localTracks: Array<Object>) {
|
||||
return Promise.all(localTracks.map(track =>
|
||||
conference.removeTrack(track)
|
||||
.catch((err: Error) => {
|
||||
// Local track might be already disposed by direct
|
||||
// JitsiTrack#dispose() call. So we should ignore this error
|
||||
// here.
|
||||
if (err.name !== JitsiTrackErrors.TRACK_IS_DISPOSED) {
|
||||
_reportError(
|
||||
'Failed to remove local track from conference',
|
||||
err);
|
||||
}
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a specific Error with a specific error message. While the
|
||||
* implementation merely logs the specified msg and err via the console at the
|
||||
* time of this writing, the intention of the function is to abstract the
|
||||
* reporting of errors and facilitate elaborating on it in the future.
|
||||
*
|
||||
* @param {string} msg - The error message to report.
|
||||
* @param {Error} err - The Error to report.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _reportError(msg: string, err: Error) {
|
||||
// TODO This is a good point to call some global error handler when we have
|
||||
// one.
|
||||
logger.error(msg, err);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a representation of the local participant such as her avatar (URL),
|
||||
* email address, and display name to (the remote participants of) a specific
|
||||
* conference.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux store, state, or
|
||||
* {@code getState} function.
|
||||
* @param {JitsiConference} conference - The {@code JitsiConference} to which
|
||||
* the representation of the local participant is to be sent.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function sendLocalParticipant(
|
||||
stateful: IStateful,
|
||||
conference?: IJitsiConference) {
|
||||
const {
|
||||
avatarURL,
|
||||
email,
|
||||
features,
|
||||
name
|
||||
} = getLocalParticipant(stateful) ?? {};
|
||||
|
||||
avatarURL && conference?.sendCommand(AVATAR_URL_COMMAND, {
|
||||
value: avatarURL
|
||||
});
|
||||
email && conference?.sendCommand(EMAIL_COMMAND, {
|
||||
value: email
|
||||
});
|
||||
|
||||
if (features && features['screen-sharing'] === 'true') {
|
||||
conference?.setLocalParticipantProperty('features_screen-sharing', true);
|
||||
}
|
||||
|
||||
conference?.setDisplayName(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* A safe implementation of lodash#startCase that doesn't deburr the string.
|
||||
*
|
||||
* NOTE: According to lodash roadmap, lodash v5 will have this function.
|
||||
*
|
||||
* Code based on https://github.com/lodash/lodash/blob/master/startCase.js.
|
||||
*
|
||||
* @param {string} s - The string to do start case on.
|
||||
* @returns {string}
|
||||
*/
|
||||
function safeStartCase(s = '') {
|
||||
return words(`${s}`.replace(/['\u2019]/g, '')).reduce(
|
||||
(result, word, index) => result + (index ? ' ' : '') + upperFirst(word)
|
||||
, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the mute state of the track based on the start muted policy.
|
||||
*
|
||||
* @param {Object|Function} stateful - Either the whole Redux state object or the Redux store's {@code getState} method.
|
||||
* @param {Function} dispatch - Redux dispatch function.
|
||||
* @param {boolean} isAudio - Whether the track is audio or video.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function updateTrackMuteState(stateful: IStateful, dispatch: IStore['dispatch'], isAudio: boolean) {
|
||||
const state = toState(stateful);
|
||||
const mutedPolicyKey = isAudio ? 'startAudioMutedPolicy' : 'startVideoMutedPolicy';
|
||||
const mutedPolicyValue = state['features/base/conference'][mutedPolicyKey];
|
||||
|
||||
// Currently, the policy only supports force muting others, not unmuting them.
|
||||
if (!mutedPolicyValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
let muteStateUpdated = false;
|
||||
const { muted } = isAudio ? state['features/base/media'].audio : state['features/base/media'].video;
|
||||
|
||||
if (isAudio && !Boolean(muted)) {
|
||||
dispatch(setAudioMuted(mutedPolicyValue, true));
|
||||
muteStateUpdated = true;
|
||||
} else if (!isAudio && !Boolean(muted)) {
|
||||
// TODO: Add a new authority for video mutism for the moderator case.
|
||||
dispatch(setVideoMuted(mutedPolicyValue, VIDEO_MUTISM_AUTHORITY.USER, true));
|
||||
muteStateUpdated = true;
|
||||
}
|
||||
|
||||
if (muteStateUpdated) {
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.mutedTitle',
|
||||
descriptionKey: 'notify.muted',
|
||||
uid: START_MUTED_NOTIFICATION_ID // use the same id, to make sure we show one notification
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
||||
}
|
||||
}
|
||||
3
react/features/base/conference/logger.ts
Normal file
3
react/features/base/conference/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/conference');
|
||||
795
react/features/base/conference/middleware.any.ts
Normal file
795
react/features/base/conference/middleware.any.ts
Normal file
@@ -0,0 +1,795 @@
|
||||
import i18n from 'i18next';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
// @ts-ignore
|
||||
import { MIN_ASSUMED_BANDWIDTH_BPS } from '../../../../modules/API/constants';
|
||||
import {
|
||||
ACTION_PINNED,
|
||||
ACTION_UNPINNED,
|
||||
createNotAllowedErrorEvent,
|
||||
createOfferAnswerFailedEvent,
|
||||
createPinnedEvent
|
||||
} from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { reloadNow } from '../../app/actions';
|
||||
import { IStore } from '../../app/types';
|
||||
import { removeLobbyChatParticipant } from '../../chat/actions.any';
|
||||
import { openDisplayNamePrompt } from '../../display-name/actions';
|
||||
import { isVpaasMeeting } from '../../jaas/functions';
|
||||
import { clearNotifications, showErrorNotification, showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { INotificationProps } from '../../notifications/types';
|
||||
import { hasDisplayName } from '../../prejoin/utils';
|
||||
import { stopLocalVideoRecording } from '../../recording/actions.any';
|
||||
import LocalRecordingManager from '../../recording/components/Recording/LocalRecordingManager';
|
||||
import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect';
|
||||
import { iAmVisitor } from '../../visitors/functions';
|
||||
import { overwriteConfig } from '../config/actions';
|
||||
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED, CONNECTION_WILL_CONNECT } from '../connection/actionTypes';
|
||||
import { connectionDisconnected, disconnect } from '../connection/actions';
|
||||
import { validateJwt } from '../jwt/functions';
|
||||
import { JitsiConferenceErrors, JitsiConferenceEvents, JitsiConnectionErrors } from '../lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../media/constants';
|
||||
import { PARTICIPANT_UPDATED, PIN_PARTICIPANT } from '../participants/actionTypes';
|
||||
import { PARTICIPANT_ROLE } from '../participants/constants';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
getPinnedParticipant
|
||||
} from '../participants/functions';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../redux/StateListenerRegistry';
|
||||
import { TRACK_ADDED, TRACK_REMOVED } from '../tracks/actionTypes';
|
||||
import { parseURIString } from '../util/uri';
|
||||
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_SUBJECT_CHANGED,
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
P2P_STATUS_CHANGED,
|
||||
SEND_TONES,
|
||||
SET_ASSUMED_BANDWIDTH_BPS,
|
||||
SET_PENDING_SUBJECT_CHANGE,
|
||||
SET_ROOM
|
||||
} from './actionTypes';
|
||||
import {
|
||||
authStatusChanged,
|
||||
conferenceFailed,
|
||||
conferenceWillLeave,
|
||||
createConference,
|
||||
setLocalSubject,
|
||||
setSubject,
|
||||
updateConferenceMetadata
|
||||
} from './actions';
|
||||
import { CONFERENCE_LEAVE_REASONS } from './constants';
|
||||
import {
|
||||
_addLocalTracksToConference,
|
||||
_removeLocalTracksFromConference,
|
||||
forEachConference,
|
||||
getCurrentConference,
|
||||
restoreConferenceOptions
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
import { IConferenceMetadata } from './reducer';
|
||||
|
||||
/**
|
||||
* Handler for before unload event.
|
||||
*/
|
||||
let beforeUnloadHandler: ((e?: any) => void) | undefined;
|
||||
|
||||
/**
|
||||
* Implements the middleware of the feature base/conference.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case CONFERENCE_FAILED:
|
||||
return _conferenceFailed(store, next, action);
|
||||
|
||||
case CONFERENCE_JOINED:
|
||||
return _conferenceJoined(store, next, action);
|
||||
|
||||
case CONNECTION_ESTABLISHED:
|
||||
return _connectionEstablished(store, next, action);
|
||||
|
||||
case CONNECTION_FAILED:
|
||||
return _connectionFailed(store, next, action);
|
||||
|
||||
case CONNECTION_WILL_CONNECT:
|
||||
// we are starting a new join process, let's clear the error notifications if any from any previous attempt
|
||||
store.dispatch(clearNotifications());
|
||||
break;
|
||||
|
||||
case CONFERENCE_SUBJECT_CHANGED:
|
||||
return _conferenceSubjectChanged(store, next, action);
|
||||
|
||||
case CONFERENCE_WILL_LEAVE:
|
||||
_conferenceWillLeave(store);
|
||||
break;
|
||||
|
||||
case P2P_STATUS_CHANGED:
|
||||
return _p2pStatusChanged(next, action);
|
||||
|
||||
case PARTICIPANT_UPDATED:
|
||||
return _updateLocalParticipantInConference(store, next, action);
|
||||
|
||||
case PIN_PARTICIPANT:
|
||||
return _pinParticipant(store, next, action);
|
||||
|
||||
case SEND_TONES:
|
||||
return _sendTones(store, next, action);
|
||||
|
||||
case SET_ROOM:
|
||||
return _setRoom(store, next, action);
|
||||
|
||||
case TRACK_ADDED:
|
||||
case TRACK_REMOVED:
|
||||
return _trackAddedOrRemoved(store, next, action);
|
||||
|
||||
case SET_ASSUMED_BANDWIDTH_BPS:
|
||||
return _setAssumedBandwidthBps(store, next, action);
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Set up state change listener to perform maintenance tasks when the conference
|
||||
* is left or failed.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => getCurrentConference(state),
|
||||
(conference, { dispatch }, previousConference): void => {
|
||||
if (conference && !previousConference) {
|
||||
conference.on(JitsiConferenceEvents.METADATA_UPDATED, (metadata: IConferenceMetadata) => {
|
||||
dispatch(updateConferenceMetadata(metadata));
|
||||
});
|
||||
}
|
||||
|
||||
if (conference !== previousConference) {
|
||||
dispatch(updateConferenceMetadata(null));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Makes sure to leave a failed conference in order to release any allocated
|
||||
* resources like peer connections, emit participant left events, etc.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} to the specified {@code store}.
|
||||
* @param {Action} action - The redux action {@code CONFERENCE_FAILED} which is
|
||||
* being dispatched in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _conferenceFailed({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
|
||||
const { conference, error } = action;
|
||||
|
||||
const result = next(action);
|
||||
const { enableForcedReload } = getState()['features/base/config'];
|
||||
|
||||
if (LocalRecordingManager.isRecordingLocally()) {
|
||||
dispatch(stopLocalVideoRecording());
|
||||
}
|
||||
|
||||
// Handle specific failure reasons.
|
||||
switch (error.name) {
|
||||
case JitsiConferenceErrors.CONFERENCE_RESTARTED: {
|
||||
if (enableForcedReload) {
|
||||
dispatch(showErrorNotification({
|
||||
description: 'Restart initiated because of a bridge failure',
|
||||
titleKey: 'dialog.sessionRestarted'
|
||||
}));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case JitsiConferenceErrors.CONNECTION_ERROR: {
|
||||
const [ msg ] = error.params;
|
||||
|
||||
dispatch(connectionDisconnected(getState()['features/base/connection'].connection));
|
||||
dispatch(showErrorNotification({
|
||||
descriptionArguments: { msg },
|
||||
descriptionKey: msg ? 'dialog.connectErrorWithMsg' : 'dialog.connectError',
|
||||
titleKey: 'connection.CONNFAIL'
|
||||
}));
|
||||
|
||||
break;
|
||||
}
|
||||
case JitsiConferenceErrors.CONFERENCE_MAX_USERS: {
|
||||
dispatch(showErrorNotification({
|
||||
hideErrorSupportLink: true,
|
||||
descriptionKey: 'dialog.maxUsersLimitReached',
|
||||
titleKey: 'dialog.maxUsersLimitReachedTitle'
|
||||
}));
|
||||
|
||||
// In case of max users(it can be from a visitor node), let's restore
|
||||
// oldConfig if any as we will be back to the main prosody.
|
||||
const newConfig = restoreConferenceOptions(getState);
|
||||
|
||||
if (newConfig) {
|
||||
dispatch(overwriteConfig(newConfig));
|
||||
dispatch(conferenceWillLeave(conference));
|
||||
|
||||
conference.leave()
|
||||
.then(() => dispatch(disconnect()));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case JitsiConferenceErrors.NOT_ALLOWED_ERROR: {
|
||||
const [ type, msg ] = error.params;
|
||||
|
||||
let descriptionKey;
|
||||
let titleKey = 'dialog.tokenAuthFailed';
|
||||
|
||||
if (type === JitsiConferenceErrors.AUTH_ERROR_TYPES.NO_MAIN_PARTICIPANTS) {
|
||||
descriptionKey = 'visitors.notification.noMainParticipantsDescription';
|
||||
titleKey = 'visitors.notification.noMainParticipantsTitle';
|
||||
} else if (type === JitsiConferenceErrors.AUTH_ERROR_TYPES.NO_VISITORS_LOBBY) {
|
||||
descriptionKey = 'visitors.notification.noVisitorLobby';
|
||||
} else if (type === JitsiConferenceErrors.AUTH_ERROR_TYPES.PROMOTION_NOT_ALLOWED) {
|
||||
descriptionKey = 'visitors.notification.notAllowedPromotion';
|
||||
} else if (type === JitsiConferenceErrors.AUTH_ERROR_TYPES.ROOM_CREATION_RESTRICTION) {
|
||||
descriptionKey = 'dialog.errorRoomCreationRestriction';
|
||||
}
|
||||
|
||||
dispatch(showErrorNotification({
|
||||
descriptionKey,
|
||||
hideErrorSupportLink: true,
|
||||
titleKey
|
||||
}));
|
||||
|
||||
sendAnalytics(createNotAllowedErrorEvent(type, msg));
|
||||
|
||||
break;
|
||||
}
|
||||
case JitsiConferenceErrors.OFFER_ANSWER_FAILED:
|
||||
sendAnalytics(createOfferAnswerFailedEvent());
|
||||
break;
|
||||
}
|
||||
|
||||
!error.recoverable
|
||||
&& conference?.leave(CONFERENCE_LEAVE_REASONS.UNRECOVERABLE_ERROR).catch((reason: Error) => {
|
||||
// Even though we don't care too much about the failure, it may be
|
||||
// good to know that it happen, so log it (on the info level).
|
||||
logger.info('JitsiConference.leave() rejected with:', reason);
|
||||
});
|
||||
|
||||
// FIXME: Workaround for the web version. Currently, the creation of the
|
||||
// conference is handled by /conference.js and appropriate failure handlers
|
||||
// are set there.
|
||||
if (typeof APP !== 'undefined') {
|
||||
_removeUnloadHandler(getState);
|
||||
}
|
||||
|
||||
if (enableForcedReload
|
||||
&& (error?.name === JitsiConferenceErrors.CONFERENCE_RESTARTED
|
||||
|| error?.name === JitsiConnectionErrors.SHARD_CHANGED_ERROR)) {
|
||||
dispatch(conferenceWillLeave(conference));
|
||||
dispatch(reloadNow());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does extra sync up on properties that may need to be updated after the
|
||||
* conference was joined.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} to the specified {@code store}.
|
||||
* @param {Action} action - The redux action {@code CONFERENCE_JOINED} which is
|
||||
* being dispatched in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _conferenceJoined({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
|
||||
const result = next(action);
|
||||
const { conference } = action;
|
||||
const { pendingSubjectChange } = getState()['features/base/conference'];
|
||||
const {
|
||||
disableBeforeUnloadHandlers = false,
|
||||
requireDisplayName
|
||||
} = getState()['features/base/config'];
|
||||
|
||||
dispatch(removeLobbyChatParticipant(true));
|
||||
|
||||
pendingSubjectChange && dispatch(setSubject(pendingSubjectChange));
|
||||
|
||||
// FIXME: Very dirty solution. This will work on web only.
|
||||
// When the user closes the window or quits the browser, lib-jitsi-meet
|
||||
// handles the process of leaving the conference. This is temporary solution
|
||||
// that should cover the described use case as part of the effort to
|
||||
// implement the conferenceWillLeave action for web.
|
||||
beforeUnloadHandler = (e?: any) => {
|
||||
if (LocalRecordingManager.isRecordingLocally()) {
|
||||
dispatch(stopLocalVideoRecording());
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.returnValue = null;
|
||||
}
|
||||
}
|
||||
dispatch(conferenceWillLeave(conference));
|
||||
};
|
||||
|
||||
if (!iAmVisitor(getState())) {
|
||||
// if a visitor is promoted back to main room and want to join an empty breakout room
|
||||
// we need to send iq to jicofo, so it can join/create the breakout room
|
||||
dispatch(overwriteConfig({ disableFocus: false }));
|
||||
}
|
||||
|
||||
window.addEventListener(disableBeforeUnloadHandlers ? 'unload' : 'beforeunload', beforeUnloadHandler);
|
||||
|
||||
if (requireDisplayName
|
||||
&& !getLocalParticipant(getState)?.name
|
||||
&& !conference.isHidden()) {
|
||||
dispatch(openDisplayNamePrompt({
|
||||
validateInput: hasDisplayName
|
||||
}));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature base/conference that the action
|
||||
* {@code CONNECTION_ESTABLISHED} is being dispatched within a specific redux
|
||||
* store.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} to the specified {@code store}.
|
||||
* @param {Action} action - The redux action {@code CONNECTION_ESTABLISHED}
|
||||
* which is being dispatched in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
async function _connectionEstablished({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
|
||||
const result = next(action);
|
||||
|
||||
const { tokenAuthUrl = false } = getState()['features/base/config'];
|
||||
|
||||
// if there is token auth URL defined and local participant is using jwt
|
||||
// this means it is logged in when connection is established, so we can change the state
|
||||
if (tokenAuthUrl && !isVpaasMeeting(getState())) {
|
||||
let email;
|
||||
|
||||
if (getState()['features/base/jwt'].jwt) {
|
||||
email = getLocalParticipant(getState())?.email;
|
||||
}
|
||||
|
||||
dispatch(authStatusChanged(true, email || ''));
|
||||
}
|
||||
|
||||
// FIXME: Workaround for the web version. Currently, the creation of the
|
||||
// conference is handled by /conference.js.
|
||||
if (typeof APP === 'undefined') {
|
||||
dispatch(createConference());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs jwt validation errors from xmpp and from the client-side validator.
|
||||
*
|
||||
* @param {string} message - The error message from xmpp.
|
||||
* @param {string} errors - The detailed errors.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _logJwtErrors(message: string, errors: string) {
|
||||
message && logger.error(`JWT error: ${message}`);
|
||||
errors && logger.error('JWT parsing errors:', errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature base/conference that the action
|
||||
* {@code CONNECTION_FAILED} is being dispatched within a specific redux
|
||||
* store.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} to the specified {@code store}.
|
||||
* @param {Action} action - The redux action {@code CONNECTION_FAILED} which is
|
||||
* being dispatched in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _connectionFailed({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
|
||||
const { connection, error } = action;
|
||||
const { jwt } = getState()['features/base/jwt'];
|
||||
|
||||
if (jwt) {
|
||||
const errors: string = validateJwt(jwt).map((err: any) =>
|
||||
i18n.t(`dialog.tokenAuthFailedReason.${err.key}`, err.args))
|
||||
.join(' ');
|
||||
|
||||
_logJwtErrors(error.message, errors);
|
||||
|
||||
// do not show the notification when we will prompt the user
|
||||
// for username and password
|
||||
if (error.name === JitsiConnectionErrors.PASSWORD_REQUIRED) {
|
||||
dispatch(showErrorNotification({
|
||||
descriptionKey: errors ? 'dialog.tokenAuthFailedWithReasons' : 'dialog.tokenAuthFailed',
|
||||
descriptionArguments: { reason: errors },
|
||||
titleKey: 'dialog.tokenAuthFailedTitle'
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (error.name === JitsiConnectionErrors.CONFERENCE_REQUEST_FAILED) {
|
||||
let notificationAction: Function = showNotification;
|
||||
const notificationProps = {
|
||||
customActionNameKey: [ 'dialog.rejoinNow' ],
|
||||
customActionHandler: [ () => dispatch(reloadNow()) ],
|
||||
descriptionKey: 'notify.connectionFailed'
|
||||
} as INotificationProps;
|
||||
|
||||
const { locationURL = { href: '' } as URL } = getState()['features/base/connection'];
|
||||
const { tenant = '' } = parseURIString(locationURL.href) || {};
|
||||
|
||||
if (tenant.startsWith('-') || tenant.endsWith('-')) {
|
||||
notificationProps.descriptionKey = 'notify.invalidTenantHyphenDescription';
|
||||
notificationProps.titleKey = 'notify.invalidTenant';
|
||||
notificationAction = showErrorNotification;
|
||||
} else if (tenant.length > 63) {
|
||||
notificationProps.descriptionKey = 'notify.invalidTenantLengthDescription';
|
||||
notificationProps.titleKey = 'notify.invalidTenant';
|
||||
notificationAction = showErrorNotification;
|
||||
}
|
||||
|
||||
dispatch(notificationAction(notificationProps, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
}
|
||||
|
||||
const result = next(action);
|
||||
|
||||
_removeUnloadHandler(getState);
|
||||
|
||||
forEachConference(getState, conference => {
|
||||
// TODO: revisit this
|
||||
// It feels that it would make things easier if JitsiConference
|
||||
// in lib-jitsi-meet would monitor it's connection and emit
|
||||
// CONFERENCE_FAILED when it's dropped. It has more knowledge on
|
||||
// whether it can recover or not. But because the reload screen
|
||||
// and the retry logic is implemented in the app maybe it can be
|
||||
// left this way for now.
|
||||
if (conference.getConnection() === connection) {
|
||||
// XXX Note that on mobile the error type passed to
|
||||
// connectionFailed is always an object with .name property.
|
||||
// This fact needs to be checked prior to enabling this logic on
|
||||
// web.
|
||||
const conferenceAction = conferenceFailed(conference, error.name);
|
||||
|
||||
// Copy the recoverable flag if set on the CONNECTION_FAILED
|
||||
// action to not emit recoverable action caused by
|
||||
// a non-recoverable one.
|
||||
if (typeof error.recoverable !== 'undefined') {
|
||||
conferenceAction.error.recoverable = error.recoverable;
|
||||
}
|
||||
|
||||
dispatch(conferenceAction);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature base/conference that the action
|
||||
* {@code CONFERENCE_SUBJECT_CHANGED} is being dispatched within a specific
|
||||
* redux store.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} to the specified {@code store}.
|
||||
* @param {Action} action - The redux action {@code CONFERENCE_SUBJECT_CHANGED}
|
||||
* which is being dispatched in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _conferenceSubjectChanged({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
|
||||
const result = next(action);
|
||||
const { subject } = getState()['features/base/conference'];
|
||||
|
||||
if (subject) {
|
||||
dispatch({
|
||||
type: SET_PENDING_SUBJECT_CHANGE,
|
||||
subject: undefined
|
||||
});
|
||||
}
|
||||
|
||||
typeof APP === 'object' && APP.API.notifySubjectChanged(subject);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature base/conference that the action
|
||||
* {@code CONFERENCE_WILL_LEAVE} is being dispatched within a specific redux
|
||||
* store.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} store - The redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _conferenceWillLeave({ getState }: IStore) {
|
||||
_removeUnloadHandler(getState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature base/conference that the action {@code PIN_PARTICIPANT}
|
||||
* is being dispatched within a specific redux store. Pins the specified remote
|
||||
* participant in the associated conference, ignores the local participant.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} to the specified {@code store}.
|
||||
* @param {Action} action - The redux action {@code PIN_PARTICIPANT} which is
|
||||
* being dispatched in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _pinParticipant({ getState }: IStore, next: Function, action: AnyAction) {
|
||||
const state = getState();
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
if (!conference) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
const id = action.participant.id;
|
||||
const participantById = getParticipantById(state, id);
|
||||
const pinnedParticipant = getPinnedParticipant(state);
|
||||
const actionName = id ? ACTION_PINNED : ACTION_UNPINNED;
|
||||
const local
|
||||
= participantById?.local
|
||||
|| (!id && pinnedParticipant?.local);
|
||||
let participantIdForEvent;
|
||||
|
||||
if (local) {
|
||||
participantIdForEvent = local;
|
||||
} else {
|
||||
participantIdForEvent
|
||||
= actionName === ACTION_PINNED ? id : pinnedParticipant?.id;
|
||||
}
|
||||
|
||||
sendAnalytics(createPinnedEvent(
|
||||
actionName,
|
||||
participantIdForEvent,
|
||||
{
|
||||
local,
|
||||
'participant_count': conference.getParticipantCount()
|
||||
}));
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the unload handler.
|
||||
*
|
||||
* @param {Function} getState - The redux getState function.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _removeUnloadHandler(getState: IStore['getState']) {
|
||||
if (typeof beforeUnloadHandler !== 'undefined') {
|
||||
const { disableBeforeUnloadHandlers = false } = getState()['features/base/config'];
|
||||
|
||||
window.removeEventListener(disableBeforeUnloadHandlers ? 'unload' : 'beforeunload', beforeUnloadHandler);
|
||||
beforeUnloadHandler = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the specified tones to be played.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} to the specified {@code store}.
|
||||
* @param {Action} action - The redux action {@code SEND_TONES} which is
|
||||
* being dispatched in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _sendTones({ getState }: IStore, next: Function, action: AnyAction) {
|
||||
const state = getState();
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
if (conference) {
|
||||
const { duration, tones, pause } = action;
|
||||
|
||||
conference.sendTones(tones, duration, pause);
|
||||
}
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature base/conference that the action
|
||||
* {@code SET_ROOM} is being dispatched within a specific
|
||||
* redux store.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} to the specified {@code store}.
|
||||
* @param {Action} action - The redux action {@code SET_ROOM}
|
||||
* which is being dispatched in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _setRoom({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
|
||||
const state = getState();
|
||||
const { localSubject, subject } = state['features/base/config'];
|
||||
const { room } = action;
|
||||
|
||||
if (room) {
|
||||
// Set the stored subject.
|
||||
localSubject && dispatch(setLocalSubject(localSubject));
|
||||
subject && dispatch(setSubject(subject));
|
||||
}
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature base/conference that the action {@code TRACK_ADDED}
|
||||
* or {@code TRACK_REMOVED} is being dispatched within a specific redux store.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} to the specified {@code store}.
|
||||
* @param {Action} action - The redux action {@code TRACK_ADDED} or
|
||||
* {@code TRACK_REMOVED} which is being dispatched in the specified
|
||||
* {@code store}.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
async function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction) {
|
||||
const track = action.track;
|
||||
|
||||
// TODO All track swapping should happen here instead of conference.js.
|
||||
if (track?.local) {
|
||||
const { getState } = store;
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
if (conference) {
|
||||
const jitsiTrack = action.track.jitsiTrack;
|
||||
|
||||
if (action.type === TRACK_ADDED) {
|
||||
// If gUM is slow and tracks are created after the user has already joined the conference, avoid
|
||||
// adding the tracks to the conference if the user is a visitor.
|
||||
if (!iAmVisitor(state)) {
|
||||
const { desktopAudioTrack } = state['features/screen-share'];
|
||||
|
||||
// If the user is sharing their screen and has a desktop audio track, we need to replace that with
|
||||
// the audio mixer effect so that the desktop audio is mixed in with the microphone audio.
|
||||
if (typeof APP !== 'undefined' && desktopAudioTrack && track.mediaType === MEDIA_TYPE.AUDIO) {
|
||||
await conference.replaceTrack(desktopAudioTrack, null);
|
||||
const audioMixerEffect = new AudioMixerEffect(desktopAudioTrack);
|
||||
|
||||
await jitsiTrack.setEffect(audioMixerEffect);
|
||||
await conference.replaceTrack(null, jitsiTrack);
|
||||
} else {
|
||||
await _addLocalTracksToConference(conference, [ jitsiTrack ]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await _removeLocalTracksFromConference(conference, [ jitsiTrack ]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the conference object when the local participant is updated.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} to the specified {@code store}.
|
||||
* @param {Action} action - The redux action which is being dispatched in the
|
||||
* specified {@code store}.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _updateLocalParticipantInConference({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
|
||||
const { conference } = getState()['features/base/conference'];
|
||||
const { participant } = action;
|
||||
const result = next(action);
|
||||
|
||||
const localParticipant = getLocalParticipant(getState);
|
||||
|
||||
if (conference && participant.id === localParticipant?.id) {
|
||||
if ('name' in participant) {
|
||||
conference.setDisplayName(participant.name);
|
||||
}
|
||||
|
||||
if ('isSilent' in participant) {
|
||||
conference.setIsSilent(participant.isSilent);
|
||||
}
|
||||
|
||||
if ('role' in participant && participant.role === PARTICIPANT_ROLE.MODERATOR) {
|
||||
const { pendingSubjectChange, subject } = getState()['features/base/conference'];
|
||||
|
||||
// When the local user role is updated to moderator and we have a pending subject change
|
||||
// which was not reflected we need to set it (the first time we tried was before becoming moderator).
|
||||
if (typeof pendingSubjectChange !== 'undefined' && pendingSubjectChange !== subject) {
|
||||
dispatch(setSubject(pendingSubjectChange));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the external API that the action {@code P2P_STATUS_CHANGED}
|
||||
* is being dispatched within a specific redux store.
|
||||
*
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} to the specified {@code store}.
|
||||
* @param {Action} action - The redux action {@code P2P_STATUS_CHANGED}
|
||||
* which is being dispatched in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _p2pStatusChanged(next: Function, action: AnyAction) {
|
||||
const result = next(action);
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyP2pStatusChanged(action.p2p);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature base/conference that the action
|
||||
* {@code SET_ASSUMED_BANDWIDTH_BPS} is being dispatched within a specific
|
||||
* redux store.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} to the specified {@code store}.
|
||||
* @param {Action} action - The redux action {@code SET_ASSUMED_BANDWIDTH_BPS}
|
||||
* which is being dispatched in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _setAssumedBandwidthBps({ getState }: IStore, next: Function, action: AnyAction) {
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const payload = Number(action.assumedBandwidthBps);
|
||||
|
||||
const assumedBandwidthBps = isNaN(payload) || payload < MIN_ASSUMED_BANDWIDTH_BPS
|
||||
? MIN_ASSUMED_BANDWIDTH_BPS
|
||||
: payload;
|
||||
|
||||
if (conference) {
|
||||
conference.setAssumedBandwidthBps(assumedBandwidthBps);
|
||||
}
|
||||
|
||||
return next(action);
|
||||
}
|
||||
46
react/features/base/conference/middleware.native.ts
Normal file
46
react/features/base/conference/middleware.native.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { appNavigate } from '../../app/actions.native';
|
||||
import { notifyConferenceFailed } from '../../conference/actions.native';
|
||||
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
|
||||
import { CONFERENCE_FAILED } from './actionTypes';
|
||||
import { conferenceLeft } from './actions.native';
|
||||
import { TRIGGER_READY_TO_CLOSE_REASONS } from './constants';
|
||||
|
||||
import './middleware.any';
|
||||
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const { dispatch } = store;
|
||||
const { error } = action;
|
||||
|
||||
switch (action.type) {
|
||||
case CONFERENCE_FAILED: {
|
||||
const { getState } = store;
|
||||
const state = getState();
|
||||
const { notifyOnConferenceDestruction = true } = state['features/base/config'];
|
||||
|
||||
if (error?.name !== JitsiConferenceErrors.CONFERENCE_DESTROYED) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!notifyOnConferenceDestruction) {
|
||||
dispatch(conferenceLeft(action.conference));
|
||||
dispatch(appNavigate(undefined));
|
||||
break;
|
||||
}
|
||||
|
||||
const [ reason ] = error.params;
|
||||
|
||||
const reasonKey = Object.keys(TRIGGER_READY_TO_CLOSE_REASONS)[
|
||||
Object.values(TRIGGER_READY_TO_CLOSE_REASONS).indexOf(reason)
|
||||
];
|
||||
|
||||
dispatch(notifyConferenceFailed(reasonKey, () => {
|
||||
dispatch(conferenceLeft(action.conference));
|
||||
dispatch(appNavigate(undefined));
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
218
react/features/base/conference/middleware.web.ts
Normal file
218
react/features/base/conference/middleware.web.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import i18next from 'i18next';
|
||||
|
||||
import {
|
||||
setPrejoinPageVisibility,
|
||||
setSkipPrejoinOnReload
|
||||
} from '../../prejoin/actions.web';
|
||||
import { isPrejoinPageVisible } from '../../prejoin/functions';
|
||||
import { iAmVisitor } from '../../visitors/functions';
|
||||
import { CONNECTION_DISCONNECTED, CONNECTION_ESTABLISHED } from '../connection/actionTypes';
|
||||
import { hangup } from '../connection/actions.web';
|
||||
import { JitsiConferenceErrors, JitsiConnectionErrors, browser } from '../lib-jitsi-meet';
|
||||
import { gumPending, setInitialGUMPromise } from '../media/actions';
|
||||
import { MEDIA_TYPE } from '../media/constants';
|
||||
import { IGUMPendingState } from '../media/types';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import { replaceLocalTrack } from '../tracks/actions.any';
|
||||
import { getLocalTracks } from '../tracks/functions.any';
|
||||
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_JOIN_IN_PROGRESS,
|
||||
CONFERENCE_LEFT,
|
||||
KICKED_OUT
|
||||
} from './actionTypes';
|
||||
import { TRIGGER_READY_TO_CLOSE_REASONS } from './constants';
|
||||
import logger from './logger';
|
||||
|
||||
import './middleware.any';
|
||||
|
||||
let screenLock: WakeLockSentinel | undefined;
|
||||
|
||||
/**
|
||||
* Releases the screen lock.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function releaseScreenLock() {
|
||||
if (screenLock) {
|
||||
if (!screenLock.released) {
|
||||
logger.debug('Releasing wake lock.');
|
||||
|
||||
try {
|
||||
await screenLock.release();
|
||||
} catch (e) {
|
||||
logger.error(`Error while releasing the screen wake lock: ${e}.`);
|
||||
}
|
||||
}
|
||||
screenLock.removeEventListener('release', onWakeLockReleased);
|
||||
screenLock = undefined;
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a new screen wake lock.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function requestWakeLock() {
|
||||
if (navigator.wakeLock?.request) {
|
||||
navigator.wakeLock.request('screen')
|
||||
.then(lock => {
|
||||
screenLock = lock;
|
||||
screenLock.addEventListener('release', onWakeLockReleased);
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
logger.debug('Wake lock created.');
|
||||
})
|
||||
.catch(e => {
|
||||
logger.error(`Error while requesting wake lock for screen: ${e}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Page visibility change handler that re-requests the wake lock if it has been released by the OS.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
async function handleVisibilityChange() {
|
||||
if (screenLock?.released && document.visibilityState === 'visible') {
|
||||
// The screen lock have been released by the OS because of document visibility change. Lets try to request the
|
||||
// wake lock again.
|
||||
await releaseScreenLock();
|
||||
requestWakeLock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wake lock released handler.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function onWakeLockReleased() {
|
||||
logger.debug('Wake lock released');
|
||||
}
|
||||
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const { dispatch, getState } = store;
|
||||
const { enableForcedReload } = getState()['features/base/config'];
|
||||
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOIN_IN_PROGRESS: {
|
||||
dispatch(setPrejoinPageVisibility(false));
|
||||
|
||||
break;
|
||||
}
|
||||
case CONFERENCE_JOINED: {
|
||||
if (enableForcedReload) {
|
||||
dispatch(setSkipPrejoinOnReload(false));
|
||||
}
|
||||
|
||||
requestWakeLock();
|
||||
|
||||
break;
|
||||
}
|
||||
case CONFERENCE_FAILED: {
|
||||
const errorName = action.error?.name;
|
||||
|
||||
if (enableForcedReload
|
||||
&& (errorName === JitsiConferenceErrors.CONFERENCE_RESTARTED
|
||||
|| errorName === JitsiConnectionErrors.SHARD_CHANGED_ERROR)) {
|
||||
dispatch(setSkipPrejoinOnReload(true));
|
||||
}
|
||||
|
||||
if (errorName === JitsiConferenceErrors.CONFERENCE_DESTROYED) {
|
||||
const state = getState();
|
||||
const { notifyOnConferenceDestruction = true } = state['features/base/config'];
|
||||
const [ reason ] = action.error.params;
|
||||
const titlekey = Object.keys(TRIGGER_READY_TO_CLOSE_REASONS)[
|
||||
Object.values(TRIGGER_READY_TO_CLOSE_REASONS).indexOf(reason)
|
||||
];
|
||||
|
||||
dispatch(hangup(true, i18next.t(titlekey) || reason, notifyOnConferenceDestruction));
|
||||
}
|
||||
|
||||
releaseScreenLock();
|
||||
|
||||
break;
|
||||
}
|
||||
case CONFERENCE_LEFT:
|
||||
case KICKED_OUT:
|
||||
releaseScreenLock();
|
||||
|
||||
break;
|
||||
case CONNECTION_DISCONNECTED: {
|
||||
const { initialGUMPromise } = getState()['features/base/media'];
|
||||
|
||||
if (initialGUMPromise) {
|
||||
store.dispatch(setInitialGUMPromise());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case CONNECTION_ESTABLISHED: {
|
||||
const { initialGUMPromise } = getState()['features/base/media'];
|
||||
const promise = initialGUMPromise ? initialGUMPromise.promise : Promise.resolve({ tracks: [] });
|
||||
const prejoinVisible = isPrejoinPageVisible(getState());
|
||||
|
||||
logger.debug(`On connection established: prejoinVisible: ${prejoinVisible}, initialGUMPromiseExists=${
|
||||
Boolean(initialGUMPromise)}, promiseExists=${Boolean(promise)}`);
|
||||
|
||||
if (prejoinVisible) {
|
||||
promise.then(() => {
|
||||
const state = getState();
|
||||
let localTracks = getLocalTracks(state['features/base/tracks']);
|
||||
const trackReplacePromises = [];
|
||||
|
||||
// Do not signal audio/video tracks if the user joins muted.
|
||||
for (const track of localTracks) {
|
||||
// Always add the audio track on Safari because of a known issue where audio playout doesn't happen
|
||||
// if the user joins audio and video muted.
|
||||
if ((track.muted && !(browser.isWebKitBased() && track.jitsiTrack
|
||||
&& track.jitsiTrack.getType() === MEDIA_TYPE.AUDIO)) || iAmVisitor(state)) {
|
||||
trackReplacePromises.push(dispatch(replaceLocalTrack(track.jitsiTrack, null))
|
||||
.catch((error: any) => {
|
||||
logger.error(`Failed to replace local track (${track.jitsiTrack}) with null: ${error}`);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Promise.allSettled(trackReplacePromises).then(() => {
|
||||
|
||||
// Re-fetch the local tracks after muted tracks have been removed above.
|
||||
// This is needed, because the tracks are effectively disposed by the replaceLocalTrack and should
|
||||
// not be used anymore.
|
||||
localTracks = getLocalTracks(getState()['features/base/tracks']);
|
||||
|
||||
const jitsiTracks = localTracks.map((t: any) => t.jitsiTrack);
|
||||
|
||||
|
||||
return APP.conference.startConference(jitsiTracks);
|
||||
})
|
||||
.catch(logger.error);
|
||||
});
|
||||
} else {
|
||||
promise.then(({ tracks }) => {
|
||||
let tracksToUse = tracks ?? [];
|
||||
|
||||
if (iAmVisitor(getState())) {
|
||||
tracksToUse = [];
|
||||
tracks.forEach(track => track.dispose().catch(logger.error));
|
||||
dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
|
||||
}
|
||||
|
||||
dispatch(setInitialGUMPromise());
|
||||
|
||||
return APP.conference.startConference(tracksToUse);
|
||||
})
|
||||
.catch(logger.error);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
704
react/features/base/conference/reducer.ts
Normal file
704
react/features/base/conference/reducer.ts
Normal file
@@ -0,0 +1,704 @@
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { FaceLandmarks } from '../../face-landmarks/types';
|
||||
import { LOCKED_LOCALLY, LOCKED_REMOTELY } from '../../room-lock/constants';
|
||||
import { ISpeakerStats } from '../../speaker-stats/reducer';
|
||||
import { SET_CONFIG } from '../config/actionTypes';
|
||||
import { IConfig } from '../config/configType';
|
||||
import { CONNECTION_WILL_CONNECT, SET_LOCATION_URL } from '../connection/actionTypes';
|
||||
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
|
||||
import ReducerRegistry from '../redux/ReducerRegistry';
|
||||
import { assign, equals, set } from '../redux/functions';
|
||||
|
||||
import {
|
||||
AUTH_STATUS_CHANGED,
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_LEFT,
|
||||
CONFERENCE_LOCAL_SUBJECT_CHANGED,
|
||||
CONFERENCE_PROPERTIES_CHANGED,
|
||||
CONFERENCE_SUBJECT_CHANGED,
|
||||
CONFERENCE_TIMESTAMP_CHANGED,
|
||||
CONFERENCE_WILL_JOIN,
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
DATA_CHANNEL_CLOSED,
|
||||
DATA_CHANNEL_OPENED,
|
||||
LOCK_STATE_CHANGED,
|
||||
P2P_STATUS_CHANGED,
|
||||
SET_ASSUMED_BANDWIDTH_BPS,
|
||||
SET_FOLLOW_ME,
|
||||
SET_FOLLOW_ME_RECORDER,
|
||||
SET_OBFUSCATED_ROOM,
|
||||
SET_PASSWORD,
|
||||
SET_PENDING_SUBJECT_CHANGE,
|
||||
SET_ROOM,
|
||||
SET_START_MUTED_POLICY,
|
||||
SET_START_REACTIONS_MUTED,
|
||||
UPDATE_CONFERENCE_METADATA
|
||||
} from './actionTypes';
|
||||
import { isRoomValid } from './functions';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
assumedBandwidthBps: undefined,
|
||||
conference: undefined,
|
||||
dataChannelOpen: undefined,
|
||||
e2eeSupported: undefined,
|
||||
joining: undefined,
|
||||
leaving: undefined,
|
||||
locked: undefined,
|
||||
membersOnly: undefined,
|
||||
metadata: undefined,
|
||||
password: undefined,
|
||||
passwordRequired: undefined,
|
||||
properties: undefined
|
||||
};
|
||||
|
||||
export interface IConferenceMetadata {
|
||||
files: {
|
||||
[fileId: string]: {
|
||||
authorParticipantJid: string;
|
||||
authorParticipantName: string;
|
||||
conferenceFullName: string;
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
fileType: string;
|
||||
progress?: number;
|
||||
timestamp: number;
|
||||
};
|
||||
};
|
||||
recording?: {
|
||||
isTranscribingEnabled: boolean;
|
||||
};
|
||||
visitors?: {
|
||||
live: boolean;
|
||||
};
|
||||
whiteboard?: {
|
||||
collabDetails: {
|
||||
roomId: string;
|
||||
roomKey: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface IJitsiConference {
|
||||
addCommandListener: Function;
|
||||
addLobbyMessageListener: Function;
|
||||
addTrack: Function;
|
||||
authenticateAndUpgradeRole: Function;
|
||||
avModerationApprove: Function;
|
||||
avModerationReject: Function;
|
||||
callUUID?: string;
|
||||
createVideoSIPGWSession: Function;
|
||||
dial: Function;
|
||||
disableAVModeration: Function;
|
||||
disableLobby: Function;
|
||||
enableAVModeration: Function;
|
||||
enableLobby: Function;
|
||||
end: Function;
|
||||
getBreakoutRooms: Function;
|
||||
getConnection: Function;
|
||||
getFileSharing: Function;
|
||||
getLocalParticipantProperty: Function;
|
||||
getLocalTracks: Function;
|
||||
getMeetingUniqueId: Function;
|
||||
getMetadataHandler: Function;
|
||||
getName: Function;
|
||||
getParticipantById: Function;
|
||||
getParticipantCount: Function;
|
||||
getParticipants: Function;
|
||||
getRole: Function;
|
||||
getShortTermCredentials: Function;
|
||||
getSpeakerStats: () => ISpeakerStats;
|
||||
getSsrcByTrack: Function;
|
||||
getTranscriptionStatus: Function;
|
||||
grantOwner: Function;
|
||||
isAVModerationSupported: Function;
|
||||
isE2EEEnabled: Function;
|
||||
isE2EESupported: Function;
|
||||
isEndConferenceSupported: Function;
|
||||
isLobbySupported: Function;
|
||||
isP2PActive: Function;
|
||||
isSIPCallingSupported: Function;
|
||||
join: Function;
|
||||
joinLobby: Function;
|
||||
kickParticipant: Function;
|
||||
leave: Function;
|
||||
lobbyApproveAccess: Function;
|
||||
lobbyDenyAccess: Function;
|
||||
lock: Function;
|
||||
markParticipantVerified: Function;
|
||||
muteParticipant: Function;
|
||||
myLobbyUserId: Function;
|
||||
myUserId: Function;
|
||||
off: Function;
|
||||
on: Function;
|
||||
options: any;
|
||||
removeTrack: Function;
|
||||
replaceTrack: Function;
|
||||
room: IJitsiConferenceRoom;
|
||||
sendApplicationLog: Function;
|
||||
sendCommand: Function;
|
||||
sendCommandOnce: Function;
|
||||
sendEndpointMessage: Function;
|
||||
sendFaceLandmarks: (faceLandmarks: FaceLandmarks) => void;
|
||||
sendFeedback: Function;
|
||||
sendLobbyMessage: Function;
|
||||
sendMessage: Function;
|
||||
sendPrivateTextMessage: Function;
|
||||
sendReaction: Function;
|
||||
sendTextMessage: Function;
|
||||
sendTones: Function;
|
||||
sessionId: string;
|
||||
setAssumedBandwidthBps: (value: number) => void;
|
||||
setDesktopSharingFrameRate: Function;
|
||||
setDisplayName: Function;
|
||||
setIsSilent: Function;
|
||||
setLocalParticipantProperty: Function;
|
||||
setMediaEncryptionKey: Function;
|
||||
setReceiverConstraints: Function;
|
||||
setSenderVideoConstraint: Function;
|
||||
setStartMutedPolicy: Function;
|
||||
setSubject: Function;
|
||||
setTranscriptionLanguage: Function;
|
||||
startRecording: Function;
|
||||
startVerification: Function;
|
||||
stopRecording: Function;
|
||||
toggleE2EE: Function;
|
||||
}
|
||||
|
||||
export interface IConferenceState {
|
||||
assumedBandwidthBps?: number;
|
||||
authEnabled?: boolean;
|
||||
authLogin?: string;
|
||||
authRequired?: IJitsiConference;
|
||||
conference?: IJitsiConference;
|
||||
conferenceTimestamp?: number;
|
||||
dataChannelOpen?: boolean;
|
||||
e2eeSupported?: boolean;
|
||||
error?: Error;
|
||||
followMeEnabled?: boolean;
|
||||
followMeRecorderEnabled?: boolean;
|
||||
joining?: IJitsiConference;
|
||||
leaving?: IJitsiConference;
|
||||
lobbyError?: boolean;
|
||||
lobbyWaitingForHost?: boolean;
|
||||
localSubject?: string;
|
||||
locked?: string;
|
||||
membersOnly?: IJitsiConference;
|
||||
metadata?: IConferenceMetadata;
|
||||
obfuscatedRoom?: string;
|
||||
obfuscatedRoomSource?: string;
|
||||
p2p?: Object;
|
||||
password?: string;
|
||||
passwordRequired?: IJitsiConference;
|
||||
pendingSubjectChange?: string;
|
||||
properties?: object;
|
||||
room?: string;
|
||||
startAudioMutedPolicy?: boolean;
|
||||
startReactionsMuted?: boolean;
|
||||
startVideoMutedPolicy?: boolean;
|
||||
subject?: string;
|
||||
}
|
||||
|
||||
export interface IJitsiConferenceRoom {
|
||||
locked: boolean;
|
||||
myroomjid: string;
|
||||
roomjid: string;
|
||||
xmpp: {
|
||||
moderator: {
|
||||
logout: Function;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface IConferenceFailedError extends Error {
|
||||
params: Array<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for actions that contain the conference object, so that it can be
|
||||
* stored for use by other action creators.
|
||||
*/
|
||||
ReducerRegistry.register<IConferenceState>('features/base/conference',
|
||||
(state = DEFAULT_STATE, action): IConferenceState => {
|
||||
switch (action.type) {
|
||||
case AUTH_STATUS_CHANGED:
|
||||
return _authStatusChanged(state, action);
|
||||
|
||||
case CONFERENCE_FAILED:
|
||||
return _conferenceFailed(state, action);
|
||||
|
||||
case CONFERENCE_JOINED:
|
||||
return _conferenceJoined(state, action);
|
||||
|
||||
case CONFERENCE_SUBJECT_CHANGED:
|
||||
return set(state, 'subject', action.subject);
|
||||
|
||||
case CONFERENCE_LOCAL_SUBJECT_CHANGED:
|
||||
return set(state, 'localSubject', action.localSubject);
|
||||
|
||||
case CONFERENCE_PROPERTIES_CHANGED:
|
||||
return _conferencePropertiesChanged(state, action);
|
||||
|
||||
case CONFERENCE_TIMESTAMP_CHANGED:
|
||||
return set(state, 'conferenceTimestamp', action.conferenceTimestamp);
|
||||
|
||||
case CONFERENCE_LEFT:
|
||||
case CONFERENCE_WILL_LEAVE:
|
||||
return _conferenceLeftOrWillLeave(state, action);
|
||||
|
||||
case CONFERENCE_WILL_JOIN:
|
||||
return _conferenceWillJoin(state, action);
|
||||
|
||||
case CONNECTION_WILL_CONNECT:
|
||||
return set(state, 'authRequired', undefined);
|
||||
|
||||
case DATA_CHANNEL_CLOSED:
|
||||
return set(state, 'dataChannelOpen', false);
|
||||
|
||||
case DATA_CHANNEL_OPENED:
|
||||
return set(state, 'dataChannelOpen', true);
|
||||
|
||||
case LOCK_STATE_CHANGED:
|
||||
return _lockStateChanged(state, action);
|
||||
|
||||
case P2P_STATUS_CHANGED:
|
||||
return _p2pStatusChanged(state, action);
|
||||
|
||||
case SET_ASSUMED_BANDWIDTH_BPS: {
|
||||
const assumedBandwidthBps = action.assumedBandwidthBps >= 0
|
||||
? Number(action.assumedBandwidthBps)
|
||||
: undefined;
|
||||
|
||||
return set(state, 'assumedBandwidthBps', assumedBandwidthBps);
|
||||
}
|
||||
case SET_FOLLOW_ME:
|
||||
return set(state, 'followMeEnabled', action.enabled);
|
||||
|
||||
case SET_FOLLOW_ME_RECORDER:
|
||||
return { ...state,
|
||||
followMeRecorderEnabled: action.enabled,
|
||||
followMeEnabled: action.enabled
|
||||
};
|
||||
|
||||
case SET_START_REACTIONS_MUTED:
|
||||
return set(state, 'startReactionsMuted', action.muted);
|
||||
|
||||
case SET_LOCATION_URL:
|
||||
return set(state, 'room', undefined);
|
||||
|
||||
case SET_OBFUSCATED_ROOM:
|
||||
return { ...state,
|
||||
obfuscatedRoom: action.obfuscatedRoom,
|
||||
obfuscatedRoomSource: action.obfuscatedRoomSource
|
||||
};
|
||||
|
||||
case SET_PASSWORD:
|
||||
return _setPassword(state, action);
|
||||
|
||||
case SET_PENDING_SUBJECT_CHANGE:
|
||||
return set(state, 'pendingSubjectChange', action.subject);
|
||||
|
||||
case SET_ROOM:
|
||||
return _setRoom(state, action);
|
||||
|
||||
case SET_START_MUTED_POLICY:
|
||||
return {
|
||||
...state,
|
||||
startAudioMutedPolicy: action.startAudioMutedPolicy,
|
||||
startVideoMutedPolicy: action.startVideoMutedPolicy
|
||||
};
|
||||
|
||||
case UPDATE_CONFERENCE_METADATA:
|
||||
return {
|
||||
...state,
|
||||
metadata: action.metadata
|
||||
};
|
||||
|
||||
case SET_CONFIG:
|
||||
return _setConfig(state, action);
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
/**
|
||||
* Processes subject and local subject of the conference based on the new config.
|
||||
*
|
||||
* @param {Object} state - The Redux state of feature base/conference.
|
||||
* @param {Action} action - The Redux action SET_CONFIG to reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state after the reduction of the specified action.
|
||||
*/
|
||||
function _setConfig(state: IConferenceState, { config }: { config: IConfig; }) {
|
||||
const { localSubject, subject } = config;
|
||||
|
||||
return {
|
||||
...state,
|
||||
localSubject,
|
||||
pendingSubjectChange: subject,
|
||||
subject: undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a specific Redux action AUTH_STATUS_CHANGED of the feature
|
||||
* base/conference.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature base/conference.
|
||||
* @param {Action} action - The Redux action AUTH_STATUS_CHANGED to reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state of the feature base/conference after the
|
||||
* reduction of the specified action.
|
||||
*/
|
||||
function _authStatusChanged(state: IConferenceState,
|
||||
{ authEnabled, authLogin }: { authEnabled: boolean; authLogin: string; }) {
|
||||
return assign(state, {
|
||||
authEnabled,
|
||||
authLogin
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a specific Redux action CONFERENCE_FAILED of the feature
|
||||
* base/conference.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature base/conference.
|
||||
* @param {Action} action - The Redux action CONFERENCE_FAILED to reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state of the feature base/conference after the
|
||||
* reduction of the specified action.
|
||||
*/
|
||||
function _conferenceFailed(state: IConferenceState, { conference, error }: {
|
||||
conference: IJitsiConference; error: IConferenceFailedError; }) {
|
||||
// The current (similar to getCurrentConference in
|
||||
// base/conference/functions.any.js) conference which is joining or joined:
|
||||
const conference_ = state.conference || state.joining;
|
||||
|
||||
if (conference_ && conference_ !== conference) {
|
||||
return state;
|
||||
}
|
||||
|
||||
let authRequired;
|
||||
let membersOnly;
|
||||
let passwordRequired;
|
||||
let lobbyWaitingForHost;
|
||||
let lobbyError;
|
||||
|
||||
switch (error.name) {
|
||||
case JitsiConferenceErrors.AUTHENTICATION_REQUIRED:
|
||||
authRequired = conference;
|
||||
break;
|
||||
|
||||
/**
|
||||
* Access denied while waiting in the lobby.
|
||||
* A conference error when we tried to join into a room with no display name when lobby is enabled in the room.
|
||||
*/
|
||||
case JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED:
|
||||
case JitsiConferenceErrors.DISPLAY_NAME_REQUIRED: {
|
||||
lobbyError = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case JitsiConferenceErrors.MEMBERS_ONLY_ERROR: {
|
||||
membersOnly = conference;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [ _lobbyJid, _lobbyWaitingForHost ] = error.params;
|
||||
|
||||
lobbyWaitingForHost = _lobbyWaitingForHost;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case JitsiConferenceErrors.PASSWORD_REQUIRED:
|
||||
passwordRequired = conference;
|
||||
break;
|
||||
}
|
||||
|
||||
return assign(state, {
|
||||
authRequired,
|
||||
conference: undefined,
|
||||
e2eeSupported: undefined,
|
||||
error,
|
||||
joining: undefined,
|
||||
leaving: undefined,
|
||||
lobbyError,
|
||||
lobbyWaitingForHost,
|
||||
|
||||
/**
|
||||
* The indicator of how the conference/room is locked. If falsy, the
|
||||
* conference/room is unlocked; otherwise, it's either
|
||||
* {@code LOCKED_LOCALLY} or {@code LOCKED_REMOTELY}.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
locked: passwordRequired ? LOCKED_REMOTELY : undefined,
|
||||
membersOnly,
|
||||
password: undefined,
|
||||
|
||||
/**
|
||||
* The JitsiConference instance which requires a password to join.
|
||||
*
|
||||
* @type {JitsiConference}
|
||||
*/
|
||||
passwordRequired
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a specific Redux action CONFERENCE_JOINED of the feature
|
||||
* base/conference.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature base/conference.
|
||||
* @param {Action} action - The Redux action CONFERENCE_JOINED to reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state of the feature base/conference after the
|
||||
* reduction of the specified action.
|
||||
*/
|
||||
function _conferenceJoined(state: IConferenceState, { conference }: { conference: IJitsiConference; }) {
|
||||
// FIXME The indicator which determines whether a JitsiConference is locked
|
||||
// i.e. password-protected is private to lib-jitsi-meet. However, the
|
||||
// library does not fire LOCK_STATE_CHANGED upon joining a JitsiConference
|
||||
// with a password.
|
||||
// FIXME Technically JitsiConference.room is a private field.
|
||||
const locked = conference.room?.locked ? LOCKED_REMOTELY : undefined;
|
||||
|
||||
return assign(state, {
|
||||
authRequired: undefined,
|
||||
|
||||
/**
|
||||
* The JitsiConference instance represented by the Redux state of the
|
||||
* feature base/conference.
|
||||
*
|
||||
* @type {JitsiConference}
|
||||
*/
|
||||
conference,
|
||||
|
||||
e2eeSupported: conference.isE2EESupported(),
|
||||
|
||||
joining: undefined,
|
||||
membersOnly: undefined,
|
||||
leaving: undefined,
|
||||
|
||||
lobbyError: undefined,
|
||||
lobbyWaitingForHost: undefined,
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the conference is locked.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
locked,
|
||||
passwordRequired: undefined
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a specific redux action {@link CONFERENCE_LEFT} or
|
||||
* {@link CONFERENCE_WILL_LEAVE} for the feature base/conference.
|
||||
*
|
||||
* @param {Object} state - The redux state of the feature base/conference.
|
||||
* @param {Action} action - The redux action {@code CONFERENCE_LEFT} or
|
||||
* {@code CONFERENCE_WILL_LEAVE} to reduce.
|
||||
* @private
|
||||
* @returns {Object} The next/new state of the feature base/conference after the
|
||||
* reduction of the specified action.
|
||||
*/
|
||||
function _conferenceLeftOrWillLeave(state: IConferenceState, { conference, type }:
|
||||
{ conference: IJitsiConference; type: string; }) {
|
||||
const nextState = { ...state };
|
||||
|
||||
// The redux action CONFERENCE_LEFT is the last time that we should be
|
||||
// hearing from a JitsiConference instance.
|
||||
//
|
||||
// The redux action CONFERENCE_WILL_LEAVE represents the order of the user
|
||||
// to leave a JitsiConference instance. From the user's perspective, there's
|
||||
// no going back (with respect to the instance itself). The app will perform
|
||||
// due clean-up like leaving the associated room, but the instance is no
|
||||
// longer the focus of the attention of the user and, consequently, the app.
|
||||
for (const p in state) {
|
||||
if (state[p as keyof IConferenceState] === conference) {
|
||||
nextState[p as keyof IConferenceState] = undefined;
|
||||
|
||||
switch (p) {
|
||||
case 'conference':
|
||||
case 'passwordRequired':
|
||||
// XXX Clear/unset locked & password for a conference which has
|
||||
// been LOCKED_LOCALLY or LOCKED_REMOTELY.
|
||||
delete nextState.locked;
|
||||
delete nextState.password;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (type === CONFERENCE_WILL_LEAVE) {
|
||||
// A CONFERENCE_WILL_LEAVE is of further consequence only if it is
|
||||
// expected i.e. if the specified conference is joining or joined.
|
||||
if (conference === state.joining || conference === state.conference) {
|
||||
/**
|
||||
* The JitsiConference instance which is currently in the process of
|
||||
* being left.
|
||||
*
|
||||
* @type {JitsiConference}
|
||||
*/
|
||||
nextState.leaving = conference;
|
||||
}
|
||||
}
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a specific Redux action CONFERENCE_PROPERTIES_CHANGED of the feature
|
||||
* base/conference.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature base/conference.
|
||||
* @param {Action} action - The Redux action CONFERENCE_PROPERTIES_CHANGED to reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state of the feature base/conference after the
|
||||
* reduction of the specified action.
|
||||
*/
|
||||
function _conferencePropertiesChanged(state: IConferenceState, { properties }: { properties: Object; }) {
|
||||
if (!equals(state.properties, properties)) {
|
||||
return assign(state, {
|
||||
properties
|
||||
});
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a specific Redux action CONFERENCE_WILL_JOIN of the feature
|
||||
* base/conference.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature base/conference.
|
||||
* @param {Action} action - The Redux action CONFERENCE_WILL_JOIN to reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state of the feature base/conference after the
|
||||
* reduction of the specified action.
|
||||
*/
|
||||
function _conferenceWillJoin(state: IConferenceState, { conference }: { conference: IJitsiConference; }) {
|
||||
return assign(state, {
|
||||
error: undefined,
|
||||
joining: conference
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a specific Redux action LOCK_STATE_CHANGED of the feature
|
||||
* base/conference.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature base/conference.
|
||||
* @param {Action} action - The Redux action LOCK_STATE_CHANGED to reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state of the feature base/conference after the
|
||||
* reduction of the specified action.
|
||||
*/
|
||||
function _lockStateChanged(state: IConferenceState, { conference, locked }: { conference: Object; locked: boolean; }) {
|
||||
if (state.conference !== conference) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return assign(state, {
|
||||
locked: locked ? state.locked || LOCKED_REMOTELY : undefined,
|
||||
password: locked ? state.password : undefined
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a specific Redux action P2P_STATUS_CHANGED of the feature
|
||||
* base/conference.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature base/conference.
|
||||
* @param {Action} action - The Redux action P2P_STATUS_CHANGED to reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state of the feature base/conference after the
|
||||
* reduction of the specified action.
|
||||
*/
|
||||
function _p2pStatusChanged(state: IConferenceState, action: AnyAction) {
|
||||
return set(state, 'p2p', action.p2p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a specific Redux action SET_PASSWORD of the feature base/conference.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature base/conference.
|
||||
* @param {Action} action - The Redux action SET_PASSWORD to reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state of the feature base/conference after the
|
||||
* reduction of the specified action.
|
||||
*/
|
||||
function _setPassword(state: IConferenceState, { conference, method, password }: {
|
||||
conference: IJitsiConference; method: Object; password: string; }) {
|
||||
switch (method) {
|
||||
case conference.join:
|
||||
return assign(state, {
|
||||
// 1. The JitsiConference which transitions away from
|
||||
// passwordRequired MUST remain in the redux state
|
||||
// features/base/conference until it transitions into
|
||||
// conference; otherwise, there is a span of time during which
|
||||
// the redux state does not even know that there is a
|
||||
// JitsiConference whatsoever.
|
||||
//
|
||||
// 2. The redux action setPassword will attempt to join the
|
||||
// JitsiConference so joining is an appropriate transitional
|
||||
// redux state.
|
||||
//
|
||||
// 3. The redux action setPassword will perform the same check
|
||||
// before it proceeds with the re-join.
|
||||
joining: state.conference ? state.joining : conference,
|
||||
locked: LOCKED_REMOTELY,
|
||||
|
||||
/**
|
||||
* The password with which the conference is to be joined.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
password
|
||||
});
|
||||
|
||||
case conference.lock:
|
||||
return assign(state, {
|
||||
locked: password ? LOCKED_LOCALLY : undefined,
|
||||
password
|
||||
});
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a specific Redux action SET_ROOM of the feature base/conference.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature base/conference.
|
||||
* @param {Action} action - The Redux action SET_ROOM to reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state of the feature base/conference after the
|
||||
* reduction of the specified action.
|
||||
*/
|
||||
function _setRoom(state: IConferenceState, action: AnyAction) {
|
||||
let { room } = action;
|
||||
|
||||
if (!isRoomValid(room)) {
|
||||
// Technically, there are multiple values which don't represent valid
|
||||
// room names. Practically, each of them is as bad as the rest of them
|
||||
// because we can't use any of them to join a conference.
|
||||
room = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the room of the conference (to be) joined.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
return assign(state, {
|
||||
error: undefined,
|
||||
room
|
||||
});
|
||||
}
|
||||
|
||||
61
react/features/base/config/actionTypes.ts
Normal file
61
react/features/base/config/actionTypes.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* The redux action which signals that a configuration (commonly known in Jitsi
|
||||
* Meet as config.js) will be loaded for a specific locationURL.
|
||||
*
|
||||
* {
|
||||
* type: CONFIG_WILL_LOAD,
|
||||
* locationURL: URL,
|
||||
* room: string
|
||||
* }
|
||||
*/
|
||||
export const CONFIG_WILL_LOAD = 'CONFIG_WILL_LOAD';
|
||||
|
||||
/**
|
||||
* The redux action which signals that a configuration (commonly known in Jitsi
|
||||
* Meet as config.js) could not be loaded due to a specific error.
|
||||
*
|
||||
* {
|
||||
* type: LOAD_CONFIG_ERROR,
|
||||
* error: Error,
|
||||
* locationURL: URL
|
||||
* }
|
||||
*/
|
||||
export const LOAD_CONFIG_ERROR = 'LOAD_CONFIG_ERROR';
|
||||
|
||||
/**
|
||||
* The redux action which sets the configuration represented by the feature
|
||||
* base/config. The configuration is defined and consumed by the library
|
||||
* lib-jitsi-meet but some of its properties are consumed by the application
|
||||
* jitsi-meet as well.
|
||||
*
|
||||
* {
|
||||
* type: SET_CONFIG,
|
||||
* config: Object
|
||||
* }
|
||||
*/
|
||||
export const SET_CONFIG = 'SET_CONFIG';
|
||||
|
||||
/**
|
||||
* The redux action which updates the configuration represented by the feature
|
||||
* base/config. The configuration is defined and consumed by the library
|
||||
* lib-jitsi-meet but some of its properties are consumed by the application
|
||||
* jitsi-meet as well. A merge operation is performed between the existing config
|
||||
* and the passed object.
|
||||
*
|
||||
* {
|
||||
* type: UPDATE_CONFIG,
|
||||
* config: Object
|
||||
* }
|
||||
*/
|
||||
export const UPDATE_CONFIG = 'UPDATE_CONFIG';
|
||||
|
||||
/**
|
||||
* The redux action which overwrites configurations represented by the feature
|
||||
* base/config. The passed on config values overwrite the current values for given props.
|
||||
*
|
||||
* {
|
||||
* type: OVERWRITE_CONFIG,
|
||||
* config: Object
|
||||
* }
|
||||
*/
|
||||
export const OVERWRITE_CONFIG = 'OVERWRITE_CONFIG';
|
||||
185
react/features/base/config/actions.ts
Normal file
185
react/features/base/config/actions.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
// @ts-expect-error
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import { addKnownDomains } from '../known-domains/actions';
|
||||
import { parseURIString } from '../util/uri';
|
||||
|
||||
import {
|
||||
CONFIG_WILL_LOAD,
|
||||
LOAD_CONFIG_ERROR,
|
||||
OVERWRITE_CONFIG,
|
||||
SET_CONFIG,
|
||||
UPDATE_CONFIG
|
||||
} from './actionTypes';
|
||||
import { IConfig } from './configType';
|
||||
import { _CONFIG_STORE_PREFIX } from './constants';
|
||||
import { setConfigFromURLParams } from './functions.any';
|
||||
|
||||
|
||||
/**
|
||||
* Updates the config with new options.
|
||||
*
|
||||
* @param {Object} config - The new options (to add).
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function updateConfig(config: IConfig) {
|
||||
return {
|
||||
type: UPDATE_CONFIG,
|
||||
config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the configuration (commonly known in Jitsi Meet as config.js)
|
||||
* for a specific locationURL will be loaded now.
|
||||
*
|
||||
* @param {URL} locationURL - The URL of the location which necessitated the
|
||||
* loading of a configuration.
|
||||
* @param {string} room - The name of the room (conference) for which we're loading the config for.
|
||||
* @returns {{
|
||||
* type: CONFIG_WILL_LOAD,
|
||||
* locationURL: URL,
|
||||
* room: string
|
||||
* }}
|
||||
*/
|
||||
export function configWillLoad(locationURL: URL, room: string) {
|
||||
return {
|
||||
type: CONFIG_WILL_LOAD,
|
||||
locationURL,
|
||||
room
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that a configuration (commonly known in Jitsi Meet as config.js)
|
||||
* could not be loaded due to a specific error.
|
||||
*
|
||||
* @param {Error} error - The {@code Error} which prevented the successful
|
||||
* loading of a configuration.
|
||||
* @param {URL} locationURL - The URL of the location which necessitated the
|
||||
* loading of a configuration.
|
||||
* @returns {{
|
||||
* type: LOAD_CONFIG_ERROR,
|
||||
* error: Error,
|
||||
* locationURL: URL
|
||||
* }}
|
||||
*/
|
||||
export function loadConfigError(error: Error, locationURL: URL) {
|
||||
return {
|
||||
type: LOAD_CONFIG_ERROR,
|
||||
error,
|
||||
locationURL
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites some config values.
|
||||
*
|
||||
* @param {Object} config - The new options (to overwrite).
|
||||
* @returns {{
|
||||
* type: OVERWRITE_CONFIG,
|
||||
* config: Object
|
||||
* }}
|
||||
*/
|
||||
export function overwriteConfig(config: Object) {
|
||||
return {
|
||||
type: OVERWRITE_CONFIG,
|
||||
config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the configuration represented by the feature base/config. The
|
||||
* configuration is defined and consumed by the library lib-jitsi-meet but some
|
||||
* of its properties are consumed by the application jitsi-meet as well.
|
||||
*
|
||||
* @param {Object} config - The configuration to be represented by the feature
|
||||
* base/config.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setConfig(config: IConfig = {}) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const { locationURL } = getState()['features/base/connection'];
|
||||
|
||||
// Now that the loading of the config was successful override the values
|
||||
// with the parameters passed in the hash part of the location URI.
|
||||
// TODO We're still in the middle ground between old Web with config,
|
||||
// and interfaceConfig used via global variables and new
|
||||
// Web and mobile reading the respective values from the redux store.
|
||||
// Only the config will be overridden on React Native, as the other
|
||||
// globals will be undefined here. It's intentional - we do not care to
|
||||
// override those configs yet.
|
||||
locationURL
|
||||
&& setConfigFromURLParams(
|
||||
|
||||
// On Web the config also comes from the window.config global,
|
||||
// but it is resolved in the loadConfig procedure.
|
||||
config,
|
||||
window.interfaceConfig,
|
||||
locationURL);
|
||||
|
||||
let { bosh } = config;
|
||||
|
||||
if (bosh) {
|
||||
// Normalize the BOSH URL.
|
||||
if (bosh.startsWith('//')) {
|
||||
// By default our config.js doesn't include the protocol.
|
||||
bosh = `${locationURL?.protocol}${bosh}`;
|
||||
} else if (bosh.startsWith('/')) {
|
||||
// Handle relative URLs, which won't work on mobile.
|
||||
const {
|
||||
protocol,
|
||||
host,
|
||||
contextRoot
|
||||
} = parseURIString(locationURL?.href);
|
||||
|
||||
bosh = `${protocol}//${host}${contextRoot || '/'}${bosh.substr(1)}`;
|
||||
}
|
||||
config.bosh = bosh;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: SET_CONFIG,
|
||||
config
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a specific Jitsi Meet config.js object into {@code localStorage}.
|
||||
*
|
||||
* @param {string} baseURL - The base URL from which the config.js was
|
||||
* downloaded.
|
||||
* @param {Object} config - The Jitsi Meet config.js to store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function storeConfig(baseURL: string, config: Object) {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
// Try to store the configuration in localStorage. If the deployment
|
||||
// specified 'getroom' as a function, for example, it does not make
|
||||
// sense to and it will not be stored.
|
||||
let b = false;
|
||||
|
||||
try {
|
||||
if (typeof window.config === 'undefined' || window.config !== config) {
|
||||
jitsiLocalStorage.setItem(`${_CONFIG_STORE_PREFIX}/${baseURL}`, JSON.stringify(config));
|
||||
b = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore the error because the caching is optional.
|
||||
}
|
||||
|
||||
// If base/config knows a domain, then the app knows it.
|
||||
if (b) {
|
||||
try {
|
||||
dispatch(addKnownDomains(parseURIString(baseURL)?.host));
|
||||
} catch (e) {
|
||||
// Ignore the error because the fiddling with "known domains" is
|
||||
// a side effect here.
|
||||
}
|
||||
}
|
||||
|
||||
return b;
|
||||
};
|
||||
}
|
||||
665
react/features/base/config/configType.ts
Normal file
665
react/features/base/config/configType.ts
Normal file
@@ -0,0 +1,665 @@
|
||||
import { ToolbarButton } from '../../toolbox/types';
|
||||
import { ILoggingConfig } from '../logging/types';
|
||||
import { DesktopSharingSourceType } from '../tracks/types';
|
||||
|
||||
type ButtonsWithNotifyClick = 'camera' |
|
||||
'chat' |
|
||||
'closedcaptions' |
|
||||
'desktop' |
|
||||
'download' |
|
||||
'embedmeeting' |
|
||||
'end-meeting' |
|
||||
'etherpad' |
|
||||
'feedback' |
|
||||
'filmstrip' |
|
||||
'fullscreen' |
|
||||
'hangup' |
|
||||
'hangup-menu' |
|
||||
'help' |
|
||||
'invite' |
|
||||
'livestreaming' |
|
||||
'microphone' |
|
||||
'mute-everyone' |
|
||||
'mute-video-everyone' |
|
||||
'participants-pane' |
|
||||
'profile' |
|
||||
'raisehand' |
|
||||
'recording' |
|
||||
'security' |
|
||||
'select-background' |
|
||||
'settings' |
|
||||
'shareaudio' |
|
||||
'sharedvideo' |
|
||||
'shortcuts' |
|
||||
'stats' |
|
||||
'tileview' |
|
||||
'toggle-camera' |
|
||||
'videoquality' |
|
||||
'add-passcode' |
|
||||
'__end';
|
||||
|
||||
type ParticipantMenuButtonsWithNotifyClick = 'allow-video' |
|
||||
'ask-unmute' |
|
||||
'conn-status' |
|
||||
'flip-local-video' |
|
||||
'grant-moderator' |
|
||||
'hide-self-view' |
|
||||
'kick' |
|
||||
'mute' |
|
||||
'mute-others' |
|
||||
'mute-others-video' |
|
||||
'mute-video' |
|
||||
'pinToStage' |
|
||||
'privateMessage' |
|
||||
'remote-control' |
|
||||
'send-participant-to-room' |
|
||||
'verify';
|
||||
|
||||
type NotifyClickButtonKey = string |
|
||||
ButtonsWithNotifyClick |
|
||||
ParticipantMenuButtonsWithNotifyClick;
|
||||
|
||||
export type NotifyClickButton = NotifyClickButtonKey |
|
||||
{
|
||||
key: NotifyClickButtonKey;
|
||||
preventExecution: boolean;
|
||||
};
|
||||
|
||||
export type Sounds = 'ASKED_TO_UNMUTE_SOUND' |
|
||||
'E2EE_OFF_SOUND' |
|
||||
'E2EE_ON_SOUND' |
|
||||
'INCOMING_MSG_SOUND' |
|
||||
'KNOCKING_PARTICIPANT_SOUND' |
|
||||
'LIVE_STREAMING_OFF_SOUND' |
|
||||
'LIVE_STREAMING_ON_SOUND' |
|
||||
'NO_AUDIO_SIGNAL_SOUND' |
|
||||
'NOISY_AUDIO_INPUT_SOUND' |
|
||||
'OUTGOING_CALL_EXPIRED_SOUND' |
|
||||
'OUTGOING_CALL_REJECTED_SOUND' |
|
||||
'OUTGOING_CALL_RINGING_SOUND' |
|
||||
'OUTGOING_CALL_START_SOUND' |
|
||||
'PARTICIPANT_JOINED_SOUND' |
|
||||
'PARTICIPANT_LEFT_SOUND' |
|
||||
'RAISE_HAND_SOUND' |
|
||||
'REACTION_SOUND' |
|
||||
'RECORDING_OFF_SOUND' |
|
||||
'RECORDING_ON_SOUND' |
|
||||
'TALK_WHILE_MUTED_SOUND';
|
||||
|
||||
export interface IDeeplinkingPlatformConfig {
|
||||
appName: string;
|
||||
appScheme: string;
|
||||
}
|
||||
|
||||
export interface IDeeplinkingMobileConfig extends IDeeplinkingPlatformConfig {
|
||||
appPackage?: string;
|
||||
downloadLink: string;
|
||||
fDroidUrl?: string;
|
||||
}
|
||||
|
||||
export interface IDesktopDownloadConfig {
|
||||
linux?: string;
|
||||
macos?: string;
|
||||
windows?: string;
|
||||
}
|
||||
|
||||
export interface IDeeplinkingDesktopConfig extends IDeeplinkingPlatformConfig {
|
||||
download?: IDesktopDownloadConfig;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface IDeeplinkingConfig {
|
||||
android?: IDeeplinkingMobileConfig;
|
||||
desktop?: IDeeplinkingDesktopConfig;
|
||||
disabled?: boolean;
|
||||
hideLogo?: boolean;
|
||||
ios?: IDeeplinkingMobileConfig;
|
||||
}
|
||||
|
||||
export type PartialRecord<K extends keyof any, T> = {
|
||||
[P in K]?: T;
|
||||
};
|
||||
|
||||
export interface INoiseSuppressionConfig {
|
||||
krisp?: {
|
||||
bufferOverflowMS?: number;
|
||||
bvc?: {
|
||||
allowedDevices?: string;
|
||||
allowedDevicesExt?: string;
|
||||
};
|
||||
debugLogs: boolean;
|
||||
enableSessionStats?: boolean;
|
||||
enabled: boolean;
|
||||
inboundModels?: PartialRecord<string, string>;
|
||||
logProcessStats?: boolean;
|
||||
models?: PartialRecord<string, string>;
|
||||
preloadInboundModels?: PartialRecord<string, string>;
|
||||
preloadModels?: PartialRecord<string, string>;
|
||||
useBVC?: boolean;
|
||||
useSharedArrayBuffer?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IWhiteboardConfig {
|
||||
collabServerBaseUrl?: string;
|
||||
enabled?: boolean;
|
||||
limitUrl?: string;
|
||||
userLimit?: number;
|
||||
}
|
||||
|
||||
export interface IWatchRTCConfiguration {
|
||||
allowBrowserLogCollection?: boolean;
|
||||
collectionInterval?: number;
|
||||
console?: {
|
||||
level: string;
|
||||
override: boolean;
|
||||
};
|
||||
debug?: boolean;
|
||||
keys?: any;
|
||||
logGetStats?: boolean;
|
||||
proxyUrl?: string;
|
||||
rtcApiKey: string;
|
||||
rtcPeerId?: string;
|
||||
rtcRoomId?: string;
|
||||
rtcTags?: string[];
|
||||
rtcToken?: string;
|
||||
wsUrl?: string;
|
||||
}
|
||||
|
||||
export interface IConfig {
|
||||
_desktopSharingSourceDevice?: string;
|
||||
_immediateReloadThreshold?: string;
|
||||
_screenshotHistoryRegionUrl?: number;
|
||||
analytics?: {
|
||||
amplitudeAPPKey?: string;
|
||||
blackListedEvents?: string[];
|
||||
disabled?: boolean;
|
||||
matomoEndpoint?: string;
|
||||
matomoSiteID?: string;
|
||||
obfuscateRoomName?: boolean;
|
||||
rtcstatsEnabled?: boolean;
|
||||
rtcstatsEndpoint?: string;
|
||||
rtcstatsLogFlushSizeBytes?: number;
|
||||
rtcstatsPollInterval?: number;
|
||||
rtcstatsSendSdp?: boolean;
|
||||
rtcstatsStoreLogs?: boolean;
|
||||
scriptURLs?: Array<string>;
|
||||
watchRTCEnabled?: boolean;
|
||||
whiteListedEvents?: string[];
|
||||
};
|
||||
apiLogLevels?: Array<'warn' | 'log' | 'error' | 'info' | 'debug'>;
|
||||
appId?: string;
|
||||
audioLevelsInterval?: number;
|
||||
audioQuality?: {
|
||||
opusMaxAverageBitrate?: number | null;
|
||||
stereo?: boolean;
|
||||
};
|
||||
autoCaptionOnRecord?: boolean;
|
||||
autoKnockLobby?: boolean;
|
||||
backgroundAlpha?: number;
|
||||
bosh?: string;
|
||||
brandingDataUrl?: string;
|
||||
brandingRoomAlias?: string;
|
||||
breakoutRooms?: {
|
||||
hideAddRoomButton?: boolean;
|
||||
hideAutoAssignButton?: boolean;
|
||||
hideJoinRoomButton?: boolean;
|
||||
};
|
||||
bridgeChannel?: {
|
||||
ignoreDomain?: string;
|
||||
preferSctp?: boolean;
|
||||
};
|
||||
buttonsWithNotifyClick?: Array<ButtonsWithNotifyClick | {
|
||||
key: ButtonsWithNotifyClick;
|
||||
preventExecution: boolean;
|
||||
}>;
|
||||
callDisplayName?: string;
|
||||
callFlowsEnabled?: boolean;
|
||||
callHandle?: string;
|
||||
callUUID?: string;
|
||||
cameraFacingMode?: string;
|
||||
channelLastN?: number;
|
||||
chromeExtensionBanner?: {
|
||||
chromeExtensionsInfo?: Array<{ id: string; path: string; }>;
|
||||
edgeUrl?: string;
|
||||
url?: string;
|
||||
};
|
||||
conferenceInfo?: {
|
||||
alwaysVisible?: Array<string>;
|
||||
autoHide?: Array<string>;
|
||||
};
|
||||
conferenceRequestUrl?: string;
|
||||
connectionIndicators?: {
|
||||
autoHide?: boolean;
|
||||
autoHideTimeout?: number;
|
||||
disableDetails?: boolean;
|
||||
disabled?: boolean;
|
||||
inactiveDisabled?: boolean;
|
||||
};
|
||||
constraints?: {
|
||||
video?: {
|
||||
height?: {
|
||||
ideal?: number;
|
||||
max?: number;
|
||||
min?: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
corsAvatarURLs?: Array<string>;
|
||||
customParticipantMenuButtons?: Array<{ icon: string; id: string; text: string; }>;
|
||||
customToolbarButtons?: Array<{ backgroundColor?: string; icon: string; id: string; text: string; }>;
|
||||
deeplinking?: IDeeplinkingConfig;
|
||||
defaultLanguage?: string;
|
||||
defaultLocalDisplayName?: string;
|
||||
defaultLogoUrl?: string;
|
||||
defaultRemoteDisplayName?: string;
|
||||
deploymentInfo?: {
|
||||
envType?: string;
|
||||
environment?: string;
|
||||
product?: string;
|
||||
region?: string;
|
||||
shard?: string;
|
||||
userRegion?: string;
|
||||
};
|
||||
deploymentUrls?: {
|
||||
downloadAppsUrl?: string;
|
||||
userDocumentationURL?: string;
|
||||
};
|
||||
desktopSharingFrameRate?: {
|
||||
max?: number;
|
||||
min?: number;
|
||||
};
|
||||
desktopSharingSources?: Array<DesktopSharingSourceType>;
|
||||
dialInConfCodeUrl?: string;
|
||||
dialInNumbersUrl?: string;
|
||||
dialOutAuthUrl?: string;
|
||||
dialOutRegionUrl?: string;
|
||||
disable1On1Mode?: boolean | null;
|
||||
disableAEC?: boolean;
|
||||
disableAGC?: boolean;
|
||||
disableAP?: boolean;
|
||||
disableAddingBackgroundImages?: boolean;
|
||||
disableAudioLevels?: boolean;
|
||||
disableBeforeUnloadHandlers?: boolean;
|
||||
disableCameraTintForeground?: boolean;
|
||||
disableChatSmileys?: boolean;
|
||||
disableDeepLinking?: boolean;
|
||||
disableFilmstripAutohiding?: boolean;
|
||||
disableFocus?: boolean;
|
||||
disableIframeAPI?: boolean;
|
||||
disableIncomingMessageSound?: boolean;
|
||||
disableInitialGUM?: boolean;
|
||||
disableInviteFunctions?: boolean;
|
||||
disableJoinLeaveSounds?: boolean;
|
||||
disableLocalVideoFlip?: boolean;
|
||||
disableModeratorIndicator?: boolean;
|
||||
disableNS?: boolean;
|
||||
disablePolls?: boolean;
|
||||
disableProfile?: boolean;
|
||||
disableReactions?: boolean;
|
||||
disableReactionsInChat?: boolean;
|
||||
disableReactionsModeration?: boolean;
|
||||
disableRecordAudioNotification?: boolean;
|
||||
disableRemoteControl?: boolean;
|
||||
disableRemoteMute?: boolean;
|
||||
disableRemoveRaisedHandOnFocus?: boolean;
|
||||
disableResponsiveTiles?: boolean;
|
||||
disableRtx?: boolean;
|
||||
disableSelfDemote?: boolean;
|
||||
disableSelfView?: boolean;
|
||||
disableSelfViewSettings?: boolean;
|
||||
disableShortcuts?: boolean;
|
||||
disableShowMoreStats?: boolean;
|
||||
disableSimulcast?: boolean;
|
||||
disableSpeakerStatsSearch?: boolean;
|
||||
disableThirdPartyRequests?: boolean;
|
||||
disableTileEnlargement?: boolean;
|
||||
disableTileView?: boolean;
|
||||
disableVirtualBackground?: boolean;
|
||||
disabledNotifications?: Array<string>;
|
||||
disabledSounds?: Array<Sounds>;
|
||||
displayJids?: boolean;
|
||||
doNotFlipLocalVideo?: boolean;
|
||||
doNotStoreRoom?: boolean;
|
||||
dropbox?: {
|
||||
appKey: string;
|
||||
redirectURI?: string;
|
||||
};
|
||||
dynamicBrandingUrl?: string;
|
||||
e2ee?: {
|
||||
disabled?: boolean;
|
||||
externallyManagedKey?: boolean;
|
||||
labels?: {
|
||||
description?: string;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
warning?: string;
|
||||
};
|
||||
};
|
||||
e2eeLabels?: {
|
||||
description?: string;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
warning?: string;
|
||||
};
|
||||
e2eping?: {
|
||||
enabled?: boolean;
|
||||
maxConferenceSize?: number;
|
||||
maxMessagesPerSecond?: number;
|
||||
numRequests?: number;
|
||||
};
|
||||
enableCalendarIntegration?: boolean;
|
||||
enableClosePage?: boolean;
|
||||
enableDisplayNameInStats?: boolean;
|
||||
enableEmailInStats?: boolean;
|
||||
enableEncodedTransformSupport?: boolean;
|
||||
enableForcedReload?: boolean;
|
||||
enableInsecureRoomNameWarning?: boolean;
|
||||
enableLobbyChat?: boolean;
|
||||
enableNoAudioDetection?: boolean;
|
||||
enableNoisyMicDetection?: boolean;
|
||||
enableOpusRed?: boolean;
|
||||
enableRemb?: boolean;
|
||||
enableSaveLogs?: boolean;
|
||||
enableTalkWhileMuted?: boolean;
|
||||
enableTcc?: boolean;
|
||||
enableWebHIDFeature?: boolean;
|
||||
enableWelcomePage?: boolean;
|
||||
etherpad_base?: string;
|
||||
faceLandmarks?: {
|
||||
captureInterval?: number;
|
||||
enableDisplayFaceExpressions?: boolean;
|
||||
enableFaceCentering?: boolean;
|
||||
enableFaceExpressionsDetection?: boolean;
|
||||
enableRTCStats?: boolean;
|
||||
faceCenteringThreshold?: number;
|
||||
};
|
||||
feedbackPercentage?: number;
|
||||
fileRecordingsServiceEnabled?: boolean;
|
||||
fileRecordingsServiceSharingEnabled?: boolean;
|
||||
fileSharing?: {
|
||||
apiUrl?: string;
|
||||
enabled?: boolean;
|
||||
maxFileSize?: number;
|
||||
};
|
||||
filmstrip?: {
|
||||
alwaysShowResizeBar?: boolean;
|
||||
disableResizable?: boolean;
|
||||
disableStageFilmstrip?: boolean;
|
||||
disableTopPanel?: boolean;
|
||||
disabled?: boolean;
|
||||
initialWidth?: number;
|
||||
minParticipantCountForTopPanel?: number;
|
||||
};
|
||||
flags?: {
|
||||
ssrcRewritingEnabled: boolean;
|
||||
};
|
||||
focusUserJid?: string;
|
||||
forceTurnRelay?: boolean;
|
||||
gatherStats?: boolean;
|
||||
giphy?: {
|
||||
displayMode?: 'all' | 'tile' | 'chat';
|
||||
enabled?: boolean;
|
||||
rating?: 'g' | 'pg' | 'pg-13' | 'r';
|
||||
sdkKey?: string;
|
||||
tileTime?: number;
|
||||
};
|
||||
googleApiApplicationClientID?: string;
|
||||
gravatar?: {
|
||||
baseUrl?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
gravatarBaseURL?: string;
|
||||
guestDialOutStatusUrl?: string;
|
||||
guestDialOutUrl?: string;
|
||||
helpCentreURL?: string;
|
||||
hiddenDomain?: string;
|
||||
hiddenPremeetingButtons?: Array<'microphone' | 'camera' | 'select-background' | 'invite' | 'settings'>;
|
||||
hideAddRoomButton?: boolean;
|
||||
hideConferenceSubject?: boolean;
|
||||
hideConferenceTimer?: boolean;
|
||||
hideDisplayName?: boolean;
|
||||
hideDominantSpeakerBadge?: boolean;
|
||||
hideEmailInSettings?: boolean;
|
||||
hideLobbyButton?: boolean;
|
||||
hideLoginButton?: boolean;
|
||||
hideParticipantsStats?: boolean;
|
||||
hideRecordingLabel?: boolean;
|
||||
hosts?: {
|
||||
anonymousdomain?: string;
|
||||
authdomain?: string;
|
||||
domain: string;
|
||||
focus?: string;
|
||||
muc: string;
|
||||
visitorFocus?: string;
|
||||
};
|
||||
iAmRecorder?: boolean;
|
||||
iAmSipGateway?: boolean;
|
||||
iAmSpot?: boolean;
|
||||
ignoreStartMuted?: boolean;
|
||||
inviteAppName?: string | null;
|
||||
inviteServiceCallFlowsUrl?: string;
|
||||
inviteServiceUrl?: string;
|
||||
jaasActuatorUrl?: string;
|
||||
jaasConferenceCreatorUrl?: string;
|
||||
jaasFeedbackMetadataURL?: string;
|
||||
jaasTokenUrl?: string;
|
||||
legalUrls?: {
|
||||
helpCentre: string;
|
||||
privacy: string;
|
||||
security: string;
|
||||
terms: string;
|
||||
};
|
||||
liveStreaming?: {
|
||||
dataPrivacyLink?: string;
|
||||
enabled?: boolean;
|
||||
helpLink?: string;
|
||||
termsLink?: string;
|
||||
validatorRegExpString?: string;
|
||||
};
|
||||
liveStreamingEnabled?: boolean;
|
||||
lobby?: {
|
||||
autoKnock?: boolean;
|
||||
enableChat?: boolean;
|
||||
};
|
||||
localRecording?: {
|
||||
disable?: boolean;
|
||||
disableSelfRecording?: boolean;
|
||||
notifyAllParticipants?: boolean;
|
||||
};
|
||||
localSubject?: string;
|
||||
locationURL?: URL;
|
||||
logging?: ILoggingConfig;
|
||||
mainToolbarButtons?: Array<Array<string>>;
|
||||
maxFullResolutionParticipants?: number;
|
||||
microsoftApiApplicationClientID?: string;
|
||||
moderatedRoomServiceUrl?: string;
|
||||
mouseMoveCallbackInterval?: number;
|
||||
noiseSuppression?: INoiseSuppressionConfig;
|
||||
noticeMessage?: string;
|
||||
notificationTimeouts?: {
|
||||
extraLong?: number;
|
||||
long?: number;
|
||||
medium?: number;
|
||||
short?: number;
|
||||
sticky?: number;
|
||||
};
|
||||
notifications?: Array<string>;
|
||||
notifyOnConferenceDestruction?: boolean;
|
||||
openSharedDocumentOnJoin?: boolean;
|
||||
opusMaxAverageBitrate?: number;
|
||||
p2p?: {
|
||||
backToP2PDelay?: number;
|
||||
codecPreferenceOrder?: Array<string>;
|
||||
enabled?: boolean;
|
||||
iceTransportPolicy?: string;
|
||||
mobileCodecPreferenceOrder?: Array<string>;
|
||||
mobileScreenshareCodec?: string;
|
||||
stunServers?: Array<{ urls: string; }>;
|
||||
};
|
||||
participantMenuButtonsWithNotifyClick?: Array<string | ParticipantMenuButtonsWithNotifyClick | {
|
||||
key: string | ParticipantMenuButtonsWithNotifyClick;
|
||||
preventExecution: boolean;
|
||||
}>;
|
||||
participantsPane?: {
|
||||
enabled?: boolean;
|
||||
hideModeratorSettingsTab?: boolean;
|
||||
hideMoreActionsButton?: boolean;
|
||||
hideMuteAllButton?: boolean;
|
||||
};
|
||||
pcStatsInterval?: number;
|
||||
peopleSearchQueryTypes?: string[];
|
||||
peopleSearchTokenLocation?: string;
|
||||
peopleSearchUrl?: string;
|
||||
preferBosh?: boolean;
|
||||
preferVisitor?: boolean;
|
||||
preferredTranscribeLanguage?: string;
|
||||
prejoinConfig?: {
|
||||
enabled?: boolean;
|
||||
hideDisplayName?: boolean;
|
||||
hideExtraJoinButtons?: Array<string>;
|
||||
preCallTestEnabled?: boolean;
|
||||
preCallTestICEUrl?: string;
|
||||
};
|
||||
raisedHands?: {
|
||||
disableLowerHandByModerator?: boolean;
|
||||
disableLowerHandNotification?: boolean;
|
||||
disableNextSpeakerNotification?: boolean;
|
||||
disableRemoveRaisedHandOnFocus?: boolean;
|
||||
};
|
||||
readOnlyName?: boolean;
|
||||
recordingLimit?: {
|
||||
appName?: string;
|
||||
appURL?: string;
|
||||
limit?: number;
|
||||
};
|
||||
recordingService?: {
|
||||
enabled?: boolean;
|
||||
hideStorageWarning?: boolean;
|
||||
sharingEnabled?: boolean;
|
||||
};
|
||||
recordingSharingUrl?: string;
|
||||
recordings?: {
|
||||
consentLearnMoreLink?: string;
|
||||
recordAudioAndVideo?: boolean;
|
||||
requireConsent?: boolean;
|
||||
showPrejoinWarning?: boolean;
|
||||
showRecordingLink?: boolean;
|
||||
skipConsentInMeeting?: boolean;
|
||||
suggestRecording?: boolean;
|
||||
};
|
||||
remoteVideoMenu?: {
|
||||
disableDemote?: boolean;
|
||||
disableGrantModerator?: boolean;
|
||||
disableKick?: boolean;
|
||||
disablePrivateChat?: 'all' | 'allow-moderator-chat' | 'disable-visitor-chat';
|
||||
disabled?: boolean;
|
||||
};
|
||||
replaceParticipant?: string;
|
||||
requireDisplayName?: boolean;
|
||||
resolution?: number;
|
||||
roomPasswordNumberOfDigits?: number;
|
||||
salesforceUrl?: string;
|
||||
screenshotCapture?: {
|
||||
enabled?: boolean;
|
||||
mode?: 'always' | 'recording';
|
||||
};
|
||||
securityUi?: {
|
||||
disableLobbyPassword?: boolean;
|
||||
hideLobbyButton?: boolean;
|
||||
};
|
||||
serviceUrl?: string;
|
||||
sharedVideoAllowedURLDomains?: Array<string>;
|
||||
sipInviteUrl?: string;
|
||||
speakerStats?: {
|
||||
disableSearch?: boolean;
|
||||
disabled?: boolean;
|
||||
order?: Array<'role' | 'name' | 'hasLeft'>;
|
||||
};
|
||||
speakerStatsOrder?: Array<'role' | 'name' | 'hasLeft'>;
|
||||
startAudioMuted?: number;
|
||||
startAudioOnly?: boolean;
|
||||
startLastN?: number;
|
||||
startScreenSharing?: boolean;
|
||||
startSilent?: boolean;
|
||||
startVideoMuted?: number;
|
||||
startWithAudioMuted?: boolean;
|
||||
startWithVideoMuted?: boolean;
|
||||
stereo?: boolean;
|
||||
subject?: string;
|
||||
testing?: {
|
||||
assumeBandwidth?: boolean;
|
||||
debugAudioLevels?: boolean;
|
||||
dumpTranscript?: boolean;
|
||||
failICE?: boolean;
|
||||
noAutoPlayVideo?: boolean;
|
||||
p2pTestMode?: boolean;
|
||||
showSpotConsentDialog?: boolean;
|
||||
skipInterimTranscriptions?: boolean;
|
||||
testMode?: boolean;
|
||||
};
|
||||
tileView?: {
|
||||
disabled?: boolean;
|
||||
numberOfVisibleTiles?: number;
|
||||
};
|
||||
tokenAuthUrl?: string;
|
||||
tokenAuthUrlAutoRedirect?: string;
|
||||
tokenGetUserInfoOutOfContext?: boolean;
|
||||
tokenLogoutUrl?: string;
|
||||
tokenRespectTenant?: boolean;
|
||||
toolbarButtons?: Array<ToolbarButton>;
|
||||
toolbarConfig?: {
|
||||
alwaysVisible?: boolean;
|
||||
autoHideWhileChatIsOpen?: boolean;
|
||||
initialTimeout?: number;
|
||||
timeout?: number;
|
||||
};
|
||||
transcribeWithAppLanguage?: boolean;
|
||||
transcribingEnabled?: boolean;
|
||||
transcription?: {
|
||||
autoCaptionOnTranscribe?: boolean;
|
||||
autoTranscribeOnRecord?: boolean;
|
||||
disableClosedCaptions?: boolean;
|
||||
enabled?: boolean;
|
||||
inviteJigasiOnBackendTranscribing?: boolean;
|
||||
preferredLanguage?: string;
|
||||
translationLanguages?: Array<string>;
|
||||
translationLanguagesHead?: Array<string>;
|
||||
useAppLanguage?: boolean;
|
||||
};
|
||||
useHostPageLocalStorage?: boolean;
|
||||
useTurnUdp?: boolean;
|
||||
videoQuality?: {
|
||||
codecPreferenceOrder?: Array<string>;
|
||||
maxBitratesVideo?: {
|
||||
[key: string]: {
|
||||
high?: number;
|
||||
low?: number;
|
||||
standard?: number;
|
||||
};
|
||||
};
|
||||
minHeightForQualityLvl?: {
|
||||
[key: number]: string;
|
||||
};
|
||||
mobileCodecPreferenceOrder?: Array<string>;
|
||||
persist?: boolean;
|
||||
};
|
||||
visitors?: {
|
||||
enableMediaOnPromote?: {
|
||||
audio?: boolean;
|
||||
video?: boolean;
|
||||
};
|
||||
queueService: string;
|
||||
};
|
||||
watchRTCConfigParams?: IWatchRTCConfiguration;
|
||||
webhookProxyUrl?: string;
|
||||
webrtcIceTcpDisable?: boolean;
|
||||
webrtcIceUdpDisable?: boolean;
|
||||
websocket?: string;
|
||||
websocketKeepAliveUrl?: string;
|
||||
welcomePage?: {
|
||||
customUrl?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
whiteboard?: IWhiteboardConfig;
|
||||
}
|
||||
254
react/features/base/config/configWhitelist.ts
Normal file
254
react/features/base/config/configWhitelist.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { isEmbedded } from '../util/embedUtils';
|
||||
|
||||
import extraConfigWhitelist from './extraConfigWhitelist';
|
||||
import isEmbeddedConfigWhitelist from './isEmbeddedConfigWhitelist';
|
||||
|
||||
/**
|
||||
* The config keys to whitelist, the keys that can be overridden.
|
||||
* Whitelisting a key allows all properties under that key to be overridden.
|
||||
* For example whitelisting 'p2p' allows 'p2p.enabled' to be overridden, and
|
||||
* overriding 'p2p.enabled' does not modify any other keys under 'p2p'.
|
||||
* The whitelist is used only for config.js.
|
||||
*
|
||||
* @type Array
|
||||
*/
|
||||
export default [
|
||||
'_desktopSharingSourceDevice',
|
||||
'_peerConnStatusOutOfLastNTimeout',
|
||||
'_peerConnStatusRtcMuteTimeout',
|
||||
'analytics.disabled',
|
||||
'analytics.rtcstatsEnabled',
|
||||
'analytics.watchRTCEnabled',
|
||||
'audioLevelsInterval',
|
||||
'audioQuality',
|
||||
'autoKnockLobby',
|
||||
'apiLogLevels',
|
||||
'avgRtpStatsN',
|
||||
'backgroundAlpha',
|
||||
'brandingRoomAlias',
|
||||
'breakoutRooms',
|
||||
'bridgeChannel',
|
||||
'buttonsWithNotifyClick',
|
||||
|
||||
/**
|
||||
* The display name of the CallKit call representing the conference/meeting
|
||||
* associated with this config.js including while the call is ongoing in the
|
||||
* UI presented by CallKit and in the system-wide call history. The property
|
||||
* is meant for use cases in which the room name is not desirable as a
|
||||
* display name for CallKit purposes and the desired display name is not
|
||||
* provided in the form of a JWT callee. As the value is associated with a
|
||||
* conference/meeting, the value makes sense not as a deployment-wide
|
||||
* configuration, only as a runtime configuration override/overwrite
|
||||
* provided by, for example, Jitsi Meet SDK for iOS.
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
'callDisplayName',
|
||||
'callFlowsEnabled',
|
||||
|
||||
/**
|
||||
* The handle
|
||||
* ({@link https://developer.apple.com/documentation/callkit/cxhandle}) of
|
||||
* the CallKit call representing the conference/meeting associated with this
|
||||
* config.js. The property is meant for use cases in which the room URL is
|
||||
* not desirable as the handle for CallKit purposes. As the value is
|
||||
* associated with a conference/meeting, the value makes sense not as a
|
||||
* deployment-wide configuration, only as a runtime configuration
|
||||
* override/overwrite provided by, for example, Jitsi Meet SDK for iOS.
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
'callHandle',
|
||||
|
||||
/**
|
||||
* The UUID of the CallKit call representing the conference/meeting
|
||||
* associated with this config.js. The property is meant for use cases in
|
||||
* which Jitsi Meet is to work with a CallKit call created outside of Jitsi
|
||||
* Meet and to be adopted by Jitsi Meet such as, for example, an incoming
|
||||
* and/or outgoing CallKit call created by Jitsi Meet SDK for iOS
|
||||
* clients/consumers prior to giving control to Jitsi Meet. As the value is
|
||||
* associated with a conference/meeting, the value makes sense not as a
|
||||
* deployment-wide configuration, only as a runtime configuration
|
||||
* override/overwrite provided by, for example, Jitsi Meet SDK for iOS.
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
'callUUID',
|
||||
|
||||
'cameraFacingMode',
|
||||
'conferenceInfo',
|
||||
'channelLastN',
|
||||
'connectionIndicators',
|
||||
'constraints',
|
||||
'deeplinking.disabled',
|
||||
'deeplinking.desktop.enabled',
|
||||
'defaultLocalDisplayName',
|
||||
'defaultRemoteDisplayName',
|
||||
'desktopSharingFrameRate',
|
||||
'desktopSharingSources',
|
||||
'disable1On1Mode',
|
||||
'disableAEC',
|
||||
'disableAGC',
|
||||
'disableAP',
|
||||
'disableAddingBackgroundImages',
|
||||
'disableAudioLevels',
|
||||
'disableBeforeUnloadHandlers',
|
||||
'disableCameraTintForeground',
|
||||
'disableChatSmileys',
|
||||
'disableDeepLinking',
|
||||
'disabledNotifications',
|
||||
'disabledSounds',
|
||||
'disableFilmstripAutohiding',
|
||||
'disableInitialGUM',
|
||||
'disableInviteFunctions',
|
||||
'disableIncomingMessageSound',
|
||||
'disableJoinLeaveSounds',
|
||||
'disableLocalVideoFlip',
|
||||
'disableModeratorIndicator',
|
||||
'disableNS',
|
||||
'disablePolls',
|
||||
'disableProfile',
|
||||
'disableReactions',
|
||||
'disableReactionsInChat',
|
||||
'disableReactionsModeration',
|
||||
'disableRecordAudioNotification',
|
||||
'disableRemoteControl',
|
||||
'disableRemoteMute',
|
||||
'disableResponsiveTiles',
|
||||
'disableRtx',
|
||||
'disableSelfDemote',
|
||||
'disableSelfView',
|
||||
'disableSelfViewSettings',
|
||||
'disableShortcuts',
|
||||
'disableShowMoreStats',
|
||||
'disableRemoveRaisedHandOnFocus',
|
||||
'disableSpeakerStatsSearch',
|
||||
'speakerStatsOrder',
|
||||
'disableSimulcast',
|
||||
'disableThirdPartyRequests',
|
||||
'disableTileView',
|
||||
'disableTileEnlargement',
|
||||
'disableVirtualBackground',
|
||||
'displayJids',
|
||||
'doNotStoreRoom',
|
||||
'doNotFlipLocalVideo',
|
||||
'dropbox.appKey',
|
||||
'e2eeLabels',
|
||||
'e2ee',
|
||||
'e2eping',
|
||||
'enableCalendarIntegration',
|
||||
'enableDisplayNameInStats',
|
||||
'enableEmailInStats',
|
||||
'enableEncodedTransformSupport',
|
||||
'enableInsecureRoomNameWarning',
|
||||
'enableLobbyChat',
|
||||
'enableOpusRed',
|
||||
'enableRemb',
|
||||
'enableSaveLogs',
|
||||
'enableTalkWhileMuted',
|
||||
'enableNoAudioDetection',
|
||||
'enableNoisyMicDetection',
|
||||
'enableTcc',
|
||||
'faceLandmarks',
|
||||
'feedbackPercentage',
|
||||
'fileSharing.enabled',
|
||||
'filmstrip',
|
||||
'flags',
|
||||
'forceTurnRelay',
|
||||
'gatherStats',
|
||||
'giphy',
|
||||
'googleApiApplicationClientID',
|
||||
'gravatar.disabled',
|
||||
'hiddenPremeetingButtons',
|
||||
'hideConferenceSubject',
|
||||
'hideDisplayName',
|
||||
'hideDominantSpeakerBadge',
|
||||
'hideRecordingLabel',
|
||||
'hideParticipantsStats',
|
||||
'hideConferenceTimer',
|
||||
'hideAddRoomButton',
|
||||
'hideEmailInSettings',
|
||||
'hideLobbyButton',
|
||||
'iAmRecorder',
|
||||
'iAmSipGateway',
|
||||
'iAmSpot',
|
||||
'ignoreStartMuted',
|
||||
'inviteAppName',
|
||||
'liveStreaming.enabled',
|
||||
'liveStreamingEnabled',
|
||||
'lobby',
|
||||
'localRecording',
|
||||
'localSubject',
|
||||
'logging',
|
||||
'mainToolbarButtons',
|
||||
'maxFullResolutionParticipants',
|
||||
'mouseMoveCallbackInterval',
|
||||
'notifications',
|
||||
'notificationTimeouts',
|
||||
'notifyOnConferenceDestruction',
|
||||
'openSharedDocumentOnJoin',
|
||||
'opusMaxAverageBitrate',
|
||||
'p2p.backToP2PDelay',
|
||||
'p2p.codecPreferenceOrder',
|
||||
'p2p.enabled',
|
||||
'p2p.iceTransportPolicy',
|
||||
'p2p.mobileCodecPreferenceOrder',
|
||||
'p2p.mobileScreenshareCodec',
|
||||
'participantMenuButtonsWithNotifyClick',
|
||||
'participantsPane',
|
||||
'pcStatsInterval',
|
||||
'preferBosh',
|
||||
'preferVisitor',
|
||||
'prejoinConfig.enabled',
|
||||
'prejoinConfig.hideDisplayName',
|
||||
'prejoinConfig.hideExtraJoinButtons',
|
||||
'raisedHands',
|
||||
'recordingService',
|
||||
'requireDisplayName',
|
||||
'remoteVideoMenu',
|
||||
'roomPasswordNumberOfDigits',
|
||||
'readOnlyName',
|
||||
'recordings.recordAudioAndVideo',
|
||||
'recordings.showPrejoinWarning',
|
||||
'recordings.showRecordingLink',
|
||||
'recordings.suggestRecording',
|
||||
'replaceParticipant',
|
||||
'resolution',
|
||||
'screenshotCapture',
|
||||
'securityUi',
|
||||
'speakerStats',
|
||||
'startAudioMuted',
|
||||
'startAudioOnly',
|
||||
'startLastN',
|
||||
'startScreenSharing',
|
||||
'startSilent',
|
||||
'startVideoMuted',
|
||||
'startWithAudioMuted',
|
||||
'startWithVideoMuted',
|
||||
'stereo',
|
||||
'subject',
|
||||
'testing',
|
||||
'toolbarButtons',
|
||||
'toolbarConfig',
|
||||
'tileView',
|
||||
'transcribingEnabled',
|
||||
'transcription',
|
||||
'useHostPageLocalStorage',
|
||||
'useTurnUdp',
|
||||
'videoQuality',
|
||||
'visitors.enableMediaOnPromote',
|
||||
'watchRTCConfigParams.allowBrowserLogCollection',
|
||||
'watchRTCConfigParams.collectionInterval',
|
||||
'watchRTCConfigParams.console',
|
||||
'watchRTCConfigParams.debug',
|
||||
'watchRTCConfigParams.keys',
|
||||
'watchRTCConfigParams.logGetStats',
|
||||
'watchRTCConfigParams.rtcApiKey',
|
||||
'watchRTCConfigParams.rtcPeerId',
|
||||
'watchRTCConfigParams.rtcRoomId',
|
||||
'watchRTCConfigParams.rtcTags',
|
||||
'watchRTCConfigParams.rtcToken',
|
||||
'webrtcIceTcpDisable',
|
||||
'webrtcIceUdpDisable',
|
||||
'whiteboard.enabled'
|
||||
].concat(extraConfigWhitelist).concat(isEmbedded() ? isEmbeddedConfigWhitelist : []);
|
||||
43
react/features/base/config/constants.ts
Normal file
43
react/features/base/config/constants.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* The prefix of the {@code localStorage} key into which {@link storeConfig}
|
||||
* stores and from which {@link restoreConfig} restores.
|
||||
*
|
||||
* @protected
|
||||
* @type string
|
||||
*/
|
||||
export const _CONFIG_STORE_PREFIX = 'config.js';
|
||||
|
||||
/**
|
||||
* The toolbar buttons to show on premeeting screens.
|
||||
*/
|
||||
export const PREMEETING_BUTTONS = [ 'microphone', 'camera', 'select-background', 'invite', 'settings' ];
|
||||
|
||||
/**
|
||||
* The toolbar buttons to show on 3rdParty prejoin screen.
|
||||
*/
|
||||
export const THIRD_PARTY_PREJOIN_BUTTONS = [ 'microphone', 'camera', 'select-background' ];
|
||||
|
||||
/**
|
||||
* The set of feature flags.
|
||||
*
|
||||
* @enum {string}
|
||||
*/
|
||||
|
||||
export const FEATURE_FLAGS = {
|
||||
SSRC_REWRITING: 'ssrcRewritingEnabled'
|
||||
};
|
||||
|
||||
/**
|
||||
* The URL at which the terms (of service/use) are available to the user.
|
||||
*/
|
||||
export const DEFAULT_TERMS_URL = 'https://jitsi.org/meet/terms';
|
||||
|
||||
/**
|
||||
* The URL at which the privacy policy is available to the user.
|
||||
*/
|
||||
export const DEFAULT_PRIVACY_URL = 'https://jitsi.org/meet/privacy';
|
||||
|
||||
/**
|
||||
* The URL at which the help centre is available to the user.
|
||||
*/
|
||||
export const DEFAULT_HELP_CENTRE_URL = 'https://web-cdn.jitsi.net/faq/meet-faq.html';
|
||||
4
react/features/base/config/extraConfigWhitelist.ts
Normal file
4
react/features/base/config/extraConfigWhitelist.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Deploy-specific configuration whitelists.
|
||||
*/
|
||||
export default [];
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Deploy-specific interface_config whitelists.
|
||||
*/
|
||||
export default [];
|
||||
453
react/features/base/config/functions.any.ts
Normal file
453
react/features/base/config/functions.any.ts
Normal file
@@ -0,0 +1,453 @@
|
||||
// @ts-ignore
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import { safeJsonParse } from '@jitsi/js-utils/json';
|
||||
import { isEmpty, mergeWith, pick } from 'lodash-es';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { getLocalParticipant } from '../participants/functions';
|
||||
import { isEmbedded } from '../util/embedUtils';
|
||||
import { parseURLParams } from '../util/parseURLParams';
|
||||
|
||||
import { IConfig } from './configType';
|
||||
import CONFIG_WHITELIST from './configWhitelist';
|
||||
import {
|
||||
DEFAULT_HELP_CENTRE_URL,
|
||||
DEFAULT_PRIVACY_URL,
|
||||
DEFAULT_TERMS_URL,
|
||||
FEATURE_FLAGS,
|
||||
_CONFIG_STORE_PREFIX
|
||||
} from './constants';
|
||||
import INTERFACE_CONFIG_WHITELIST from './interfaceConfigWhitelist';
|
||||
import logger from './logger';
|
||||
|
||||
// XXX The function getRoomName is split out of
|
||||
// functions.any.js because it is bundled in both app.bundle and
|
||||
// do_external_connect, webpack 1 does not support tree shaking, and we don't
|
||||
// want all functions to be bundled in do_external_connect.
|
||||
export { default as getRoomName } from './getRoomName';
|
||||
|
||||
/**
|
||||
* Create a "fake" configuration object for the given base URL. This is used in case the config
|
||||
* couldn't be loaded in the welcome page, so at least we have something to try with.
|
||||
*
|
||||
* @param {string} baseURL - URL of the deployment for which we want the fake config.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function createFakeConfig(baseURL: string) {
|
||||
const url = new URL(baseURL);
|
||||
|
||||
return {
|
||||
hosts: {
|
||||
domain: url.hostname,
|
||||
muc: `conference.${url.hostname}`
|
||||
},
|
||||
bosh: `${baseURL}http-bind`,
|
||||
p2p: {
|
||||
enabled: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector used to get the meeting region.
|
||||
*
|
||||
* @param {Object} state - The global state.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getMeetingRegion(state: IReduxState) {
|
||||
return state['features/base/config']?.deploymentInfo?.region || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector used to get the SSRC-rewriting feature flag.
|
||||
*
|
||||
* @param {Object} state - The global state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getSsrcRewritingFeatureFlag(state: IReduxState) {
|
||||
return getFeatureFlag(state, FEATURE_FLAGS.SSRC_REWRITING) ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector used to get a feature flag.
|
||||
*
|
||||
* @param {Object} state - The global state.
|
||||
* @param {string} featureFlag - The name of the feature flag.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getFeatureFlag(state: IReduxState, featureFlag: string) {
|
||||
const featureFlags = state['features/base/config']?.flags || {};
|
||||
|
||||
return featureFlags[featureFlag as keyof typeof featureFlags];
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector used to get the disableRemoveRaisedHandOnFocus.
|
||||
*
|
||||
* @param {Object} state - The global state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getDisableRemoveRaisedHandOnFocus(state: IReduxState) {
|
||||
return state['features/base/config']?.raisedHands?.disableRemoveRaisedHandOnFocus || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector used to get the disableLowerHandByModerator.
|
||||
*
|
||||
* @param {Object} state - The global state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getDisableLowerHandByModerator(state: IReduxState) {
|
||||
return state['features/base/config']?.raisedHands?.disableLowerHandByModerator || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector used to get the disableLowerHandNotification.
|
||||
*
|
||||
* @param {Object} state - The global state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getDisableLowerHandNotification(state: IReduxState) {
|
||||
return state['features/base/config']?.raisedHands?.disableLowerHandNotification || true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector used to get the disableNextSpeakerNotification.
|
||||
*
|
||||
* @param {Object} state - The global state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getDisableNextSpeakerNotification(state: IReduxState) {
|
||||
return state['features/base/config']?.raisedHands?.disableNextSpeakerNotification || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector used to get the endpoint used for fetching the recording.
|
||||
*
|
||||
* @param {Object} state - The global state.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getRecordingSharingUrl(state: IReduxState) {
|
||||
return state['features/base/config'].recordingSharingUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides JSON properties in {@code config} and
|
||||
* {@code interfaceConfig} Objects with the values from {@code newConfig}.
|
||||
* Overrides only the whitelisted keys.
|
||||
*
|
||||
* @param {Object} config - The config Object in which we'll be overriding
|
||||
* properties.
|
||||
* @param {Object} interfaceConfig - The interfaceConfig Object in which we'll
|
||||
* be overriding properties.
|
||||
* @param {Object} json - Object containing configuration properties.
|
||||
* Destination object is selected based on root property name:
|
||||
* {
|
||||
* config: {
|
||||
* // config.js properties here
|
||||
* },
|
||||
* interfaceConfig: {
|
||||
* // interface_config.js properties here
|
||||
* }
|
||||
* }.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function overrideConfigJSON(config: IConfig, interfaceConfig: any, json: any) {
|
||||
for (const configName of Object.keys(json)) {
|
||||
let configObj;
|
||||
|
||||
if (configName === 'config') {
|
||||
configObj = config;
|
||||
} else if (configName === 'interfaceConfig') {
|
||||
configObj = interfaceConfig;
|
||||
}
|
||||
if (configObj) {
|
||||
const configJSON
|
||||
= getWhitelistedJSON(configName as 'interfaceConfig' | 'config', json[configName]);
|
||||
|
||||
if (!isEmpty(configJSON)) {
|
||||
logger.info(`Extending ${configName} with: ${JSON.stringify(configJSON)}`);
|
||||
|
||||
// eslint-disable-next-line arrow-body-style
|
||||
mergeWith(configObj, configJSON, (oldValue, newValue) => {
|
||||
|
||||
// XXX We don't want to merge the arrays, we want to
|
||||
// overwrite them.
|
||||
return Array.isArray(oldValue) ? newValue : undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable max-params, no-shadow */
|
||||
|
||||
/**
|
||||
* Apply whitelist filtering for configs with whitelists.
|
||||
* Only extracts overridden values for keys we allow to be overridden.
|
||||
*
|
||||
* @param {string} configName - The config name, one of config or interfaceConfig.
|
||||
* @param {Object} configJSON - The object with keys and values to override.
|
||||
* @returns {Object} - The result object only with the keys
|
||||
* that are whitelisted.
|
||||
*/
|
||||
export function getWhitelistedJSON(configName: 'interfaceConfig' | 'config', configJSON: any): Object {
|
||||
// Disable whitelisting in dev mode.
|
||||
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
||||
logger.warn('Whitelisting is disabled in dev mode, accepting any overrides');
|
||||
|
||||
return configJSON;
|
||||
}
|
||||
|
||||
if (configName === 'interfaceConfig') {
|
||||
return pick(configJSON, INTERFACE_CONFIG_WHITELIST);
|
||||
} else if (configName === 'config') {
|
||||
return pick(configJSON, CONFIG_WHITELIST);
|
||||
}
|
||||
|
||||
return configJSON;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for determining if the display name is read only.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isNameReadOnly(state: IReduxState): boolean {
|
||||
return Boolean(state['features/base/config'].disableProfile
|
||||
|| state['features/base/config'].readOnlyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for determining if the participant is the next one in the queue to speak.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isNextToSpeak(state: IReduxState): boolean {
|
||||
const raisedHandsQueue = state['features/base/participants'].raisedHandsQueue || [];
|
||||
const participantId = getLocalParticipant(state)?.id;
|
||||
|
||||
return participantId === raisedHandsQueue[0]?.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for determining if the next to speak participant in the queue has been notified.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function hasBeenNotified(state: IReduxState): boolean {
|
||||
const raisedHandsQueue = state['features/base/participants'].raisedHandsQueue;
|
||||
|
||||
return Boolean(raisedHandsQueue[0]?.hasBeenNotified);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for determining if the display name is visible.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isDisplayNameVisible(state: IReduxState): boolean {
|
||||
return !state['features/base/config'].hideDisplayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a Jitsi Meet config.js from {@code localStorage} if it was
|
||||
* previously downloaded from a specific {@code baseURL} and stored with
|
||||
* {@link storeConfig}.
|
||||
*
|
||||
* @param {string} baseURL - The base URL from which the config.js was
|
||||
* previously downloaded and stored with {@code storeConfig}.
|
||||
* @returns {?Object} The Jitsi Meet config.js which was previously downloaded
|
||||
* from {@code baseURL} and stored with {@code storeConfig} if it was restored;
|
||||
* otherwise, {@code undefined}.
|
||||
*/
|
||||
export function restoreConfig(baseURL: string) {
|
||||
const key = `${_CONFIG_STORE_PREFIX}/${baseURL}`;
|
||||
const config = jitsiLocalStorage.getItem(key);
|
||||
|
||||
if (config) {
|
||||
try {
|
||||
return safeJsonParse(config) || undefined;
|
||||
} catch (e) {
|
||||
// Somehow incorrect data ended up in the storage. Clean it up.
|
||||
jitsiLocalStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspects the hash part of the location URI and overrides values specified
|
||||
* there in the corresponding config objects given as the arguments. The syntax
|
||||
* is: {@code https://server.com/room#config.debug=true
|
||||
* &interfaceConfig.showButton=false}.
|
||||
*
|
||||
* In the hash part each parameter will be parsed to JSON and then the root
|
||||
* object will be matched with the corresponding config object given as the
|
||||
* argument to this function.
|
||||
*
|
||||
* @param {Object} config - This is the general config.
|
||||
* @param {Object} interfaceConfig - This is the interface config.
|
||||
* @param {URI} location - The new location to which the app is navigating to.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function setConfigFromURLParams(
|
||||
config: IConfig, interfaceConfig: any, location: string | URL) {
|
||||
const params = parseURLParams(location);
|
||||
const json: any = {};
|
||||
|
||||
// At this point we have:
|
||||
// params = {
|
||||
// "config.disableAudioLevels": false,
|
||||
// "config.channelLastN": -1,
|
||||
// "interfaceConfig.APP_NAME": "Jitsi Meet"
|
||||
// }
|
||||
// We want to have:
|
||||
// json = {
|
||||
// config: {
|
||||
// "disableAudioLevels": false,
|
||||
// "channelLastN": -1
|
||||
// },
|
||||
// interfaceConfig: {
|
||||
// "APP_NAME": "Jitsi Meet"
|
||||
// }
|
||||
// }
|
||||
config && (json.config = {});
|
||||
interfaceConfig && (json.interfaceConfig = {});
|
||||
|
||||
for (const param of Object.keys(params)) {
|
||||
let base = json;
|
||||
const names = param.split('.');
|
||||
const last = names.pop() ?? '';
|
||||
|
||||
for (const name of names) {
|
||||
base = base[name] = base[name] || {};
|
||||
}
|
||||
|
||||
base[last] = params[param];
|
||||
}
|
||||
|
||||
overrideConfigJSON(config, interfaceConfig, json);
|
||||
|
||||
// Print warning about deprecated URL params
|
||||
if ('interfaceConfig.SUPPORT_URL' in params) {
|
||||
logger.warn('Using SUPPORT_URL interfaceConfig URL overwrite is deprecated.'
|
||||
+ ' Please use supportUrl from advanced branding!');
|
||||
}
|
||||
|
||||
if ('config.defaultLogoUrl' in params) {
|
||||
logger.warn('Using defaultLogoUrl config URL overwrite is deprecated.'
|
||||
+ ' Please use logoImageUrl from advanced branding!');
|
||||
}
|
||||
|
||||
const deploymentUrlsConfig = params['config.deploymentUrls'] ?? {};
|
||||
|
||||
if ('config.deploymentUrls.downloadAppsUrl' in params || 'config.deploymentUrls.userDocumentationURL' in params
|
||||
|| (typeof deploymentUrlsConfig === 'object'
|
||||
&& ('downloadAppsUrl' in deploymentUrlsConfig || 'userDocumentationURL' in deploymentUrlsConfig))) {
|
||||
logger.warn('Using deploymentUrls config URL overwrite is deprecated.'
|
||||
+ ' Please use downloadAppsUrl and/or userDocumentationURL from advanced branding!');
|
||||
}
|
||||
|
||||
const liveStreamingConfig = params['config.liveStreaming'] ?? {};
|
||||
|
||||
if (('interfaceConfig.LIVE_STREAMING_HELP_LINK' in params)
|
||||
|| ('config.liveStreaming.termsLink' in params)
|
||||
|| ('config.liveStreaming.dataPrivacyLink' in params)
|
||||
|| ('config.liveStreaming.helpLink' in params)
|
||||
|| (typeof params['config.liveStreaming'] === 'object' && 'config.liveStreaming' in params
|
||||
&& (
|
||||
'termsLink' in liveStreamingConfig
|
||||
|| 'dataPrivacyLink' in liveStreamingConfig
|
||||
|| 'helpLink' in liveStreamingConfig
|
||||
)
|
||||
)) {
|
||||
logger.warn('Using liveStreaming config URL overwrite and/or LIVE_STREAMING_HELP_LINK interfaceConfig URL'
|
||||
+ ' overwrite is deprecated. Please use liveStreaming from advanced branding!');
|
||||
}
|
||||
|
||||
// When not in an iframe, start without media if the pre-join page is not enabled.
|
||||
if (!isEmbedded()
|
||||
&& 'config.prejoinConfig.enabled' in params && config.prejoinConfig?.enabled === false) {
|
||||
logger.warn('Using prejoinConfig.enabled config URL overwrite implies starting without media.');
|
||||
config.disableInitialGUM = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable max-params */
|
||||
|
||||
/**
|
||||
* Returns the dial out url.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getDialOutStatusUrl(state: IReduxState) {
|
||||
return state['features/base/config'].guestDialOutStatusUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the dial out status url.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getDialOutUrl(state: IReduxState) {
|
||||
return state['features/base/config'].guestDialOutUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector to return the security UI config.
|
||||
*
|
||||
* @param {IReduxState} state - State object.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getSecurityUiConfig(state: IReduxState) {
|
||||
return state['features/base/config']?.securityUi || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the terms, privacy and help centre URL's.
|
||||
*
|
||||
* @param {IReduxState} state - The state of the application.
|
||||
* @returns {{
|
||||
* privacy: string,
|
||||
* helpCentre: string,
|
||||
* terms: string
|
||||
* }}
|
||||
*/
|
||||
export function getLegalUrls(state: IReduxState) {
|
||||
const helpCentreURL = state['features/base/config']?.helpCentreURL;
|
||||
const configLegalUrls = state['features/base/config']?.legalUrls;
|
||||
|
||||
return {
|
||||
privacy: configLegalUrls?.privacy || DEFAULT_PRIVACY_URL,
|
||||
helpCentre: helpCentreURL || configLegalUrls?.helpCentre || DEFAULT_HELP_CENTRE_URL,
|
||||
terms: configLegalUrls?.terms || DEFAULT_TERMS_URL
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to debounce the execution of a callback function.
|
||||
*
|
||||
* @param {Function} callback - The callback to debounce.
|
||||
* @param {number} delay - The debounce delay in milliseconds.
|
||||
* @returns {Function} - A debounced function that delays the execution of the callback.
|
||||
*/
|
||||
export function debounce(callback: (...args: any[]) => void, delay: number) {
|
||||
let timerId: any;
|
||||
|
||||
return (...args: any[]) => {
|
||||
if (timerId) {
|
||||
clearTimeout(timerId);
|
||||
}
|
||||
timerId = setTimeout(() => callback(...args), delay);
|
||||
};
|
||||
}
|
||||
53
react/features/base/config/functions.native.ts
Normal file
53
react/features/base/config/functions.native.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { REPLACE_PARTICIPANT } from '../flags/constants';
|
||||
import { getFeatureFlag } from '../flags/functions';
|
||||
|
||||
import { IConfig, IDeeplinkingConfig } from './configType';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
/**
|
||||
* Removes all analytics related options from the given configuration, in case of a libre build.
|
||||
*
|
||||
* @param {*} config - The configuration which needs to be cleaned up.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function _cleanupConfig(config: IConfig) {
|
||||
config.analytics = config.analytics ?? {};
|
||||
config.analytics.scriptURLs = [];
|
||||
|
||||
if (NativeModules.AppInfo.LIBRE_BUILD) {
|
||||
delete config.analytics?.amplitudeAPPKey;
|
||||
delete config.analytics?.rtcstatsEnabled;
|
||||
delete config.analytics?.rtcstatsEndpoint;
|
||||
delete config.analytics?.rtcstatsPollInterval;
|
||||
delete config.analytics?.rtcstatsSendSdp;
|
||||
delete config.analytics?.obfuscateRoomName;
|
||||
delete config.analytics?.watchRTCEnabled;
|
||||
delete config.watchRTCConfigParams;
|
||||
config.giphy = { enabled: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the replaceParticipant config.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getReplaceParticipant(state: IReduxState): string {
|
||||
return getFeatureFlag(state, REPLACE_PARTICIPANT, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the defaults for deeplinking.
|
||||
*
|
||||
* @param {IDeeplinkingConfig} _deeplinking - The deeplinking config.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function _setDeeplinkingDefaults(_deeplinking: IDeeplinkingConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
87
react/features/base/config/functions.web.ts
Normal file
87
react/features/base/config/functions.web.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { IReduxState } from '../../app/types';
|
||||
import JitsiMeetJS from '../../base/lib-jitsi-meet';
|
||||
|
||||
import {
|
||||
IConfig,
|
||||
IDeeplinkingConfig,
|
||||
IDeeplinkingDesktopConfig,
|
||||
IDeeplinkingMobileConfig
|
||||
} from './configType';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
/**
|
||||
* Removes all analytics related options from the given configuration, in case of a libre build.
|
||||
*
|
||||
* @param {*} _config - The configuration which needs to be cleaned up.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function _cleanupConfig(_config: IConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the replaceParticipant config.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getReplaceParticipant(state: IReduxState): string | undefined {
|
||||
return state['features/base/config'].replaceParticipant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configuration value of web-hid feature.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean} True if web-hid feature should be enabled, otherwise false.
|
||||
*/
|
||||
export function getWebHIDFeatureConfig(state: IReduxState): boolean {
|
||||
return state['features/base/config'].enableWebHIDFeature || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether audio level measurement is enabled or not.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function areAudioLevelsEnabled(state: IReduxState): boolean {
|
||||
return !state['features/base/config'].disableAudioLevels && JitsiMeetJS.isCollectingLocalStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the defaults for deeplinking.
|
||||
*
|
||||
* @param {IDeeplinkingConfig} deeplinking - The deeplinking config.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function _setDeeplinkingDefaults(deeplinking: IDeeplinkingConfig) {
|
||||
deeplinking.desktop = deeplinking.desktop || {} as IDeeplinkingDesktopConfig;
|
||||
deeplinking.android = deeplinking.android || {} as IDeeplinkingMobileConfig;
|
||||
deeplinking.ios = deeplinking.ios || {} as IDeeplinkingMobileConfig;
|
||||
|
||||
const { android, desktop, ios } = deeplinking;
|
||||
|
||||
desktop.appName = desktop.appName || 'Jitsi Meet';
|
||||
desktop.appScheme = desktop.appScheme || 'jitsi-meet';
|
||||
desktop.download = desktop.download || {};
|
||||
desktop.download.windows = desktop.download.windows
|
||||
|| 'https://github.com/jitsi/jitsi-meet-electron/releases/latest/download/jitsi-meet.exe';
|
||||
desktop.download.macos = desktop.download.macos
|
||||
|| 'https://github.com/jitsi/jitsi-meet-electron/releases/latest/download/jitsi-meet.dmg';
|
||||
desktop.download.linux = desktop.download.linux
|
||||
|| 'https://github.com/jitsi/jitsi-meet-electron/releases/latest/download/jitsi-meet-x86_64.AppImage';
|
||||
|
||||
ios.appName = ios.appName || 'Jitsi Meet';
|
||||
ios.appScheme = ios.appScheme || 'org.jitsi.meet';
|
||||
ios.downloadLink = ios.downloadLink
|
||||
|| 'https://itunes.apple.com/us/app/jitsi-meet/id1165103905';
|
||||
|
||||
android.appName = android.appName || 'Jitsi Meet';
|
||||
android.appScheme = android.appScheme || 'org.jitsi.meet';
|
||||
android.downloadLink = android.downloadLink
|
||||
|| 'https://play.google.com/store/apps/details?id=org.jitsi.meet';
|
||||
android.appPackage = android.appPackage || 'org.jitsi.meet';
|
||||
android.fDroidUrl = android.fDroidUrl || 'https://f-droid.org/packages/org.jitsi.meet/';
|
||||
}
|
||||
15
react/features/base/config/getRoomName.ts
Normal file
15
react/features/base/config/getRoomName.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { getBackendSafeRoomName } from '../util/uri';
|
||||
|
||||
/**
|
||||
* Builds and returns the room name.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export default function getRoomName(): string | undefined {
|
||||
const path = window.location.pathname;
|
||||
|
||||
// The last non-directory component of the path (name) is the room.
|
||||
const roomName = path.substring(path.lastIndexOf('/') + 1) || undefined;
|
||||
|
||||
return getBackendSafeRoomName(roomName);
|
||||
}
|
||||
57
react/features/base/config/interfaceConfigWhitelist.ts
Normal file
57
react/features/base/config/interfaceConfigWhitelist.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { isEmbedded } from '../util/embedUtils';
|
||||
|
||||
import extraInterfaceConfigWhitelistCopy from './extraInterfaceConfigWhitelist';
|
||||
import isEmbeddedInterfaceConfigWhitelist from './isEmbeddedInterfaceConfigWhitelist';
|
||||
|
||||
/**
|
||||
* The interface config keys to whitelist, the keys that can be overridden.
|
||||
*
|
||||
* @private
|
||||
* @type Array
|
||||
*/
|
||||
export default [
|
||||
'AUDIO_LEVEL_PRIMARY_COLOR',
|
||||
'AUDIO_LEVEL_SECONDARY_COLOR',
|
||||
'AUTO_PIN_LATEST_SCREEN_SHARE',
|
||||
'CLOSE_PAGE_GUEST_HINT',
|
||||
'CONNECTION_INDICATOR_AUTO_HIDE_ENABLED',
|
||||
'CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT',
|
||||
'CONNECTION_INDICATOR_DISABLED',
|
||||
'DEFAULT_BACKGROUND',
|
||||
'DISABLE_PRESENCE_STATUS',
|
||||
'DISABLE_JOIN_LEAVE_NOTIFICATIONS',
|
||||
'DEFAULT_LOCAL_DISPLAY_NAME',
|
||||
'DEFAULT_REMOTE_DISPLAY_NAME',
|
||||
'DISABLE_DOMINANT_SPEAKER_INDICATOR',
|
||||
'DISABLE_FOCUS_INDICATOR',
|
||||
'DISABLE_PRIVATE_MESSAGES',
|
||||
'DISABLE_TRANSCRIPTION_SUBTITLES',
|
||||
'DISABLE_VIDEO_BACKGROUND',
|
||||
'DISPLAY_WELCOME_PAGE_CONTENT',
|
||||
'ENABLE_DIAL_OUT',
|
||||
'FILM_STRIP_MAX_HEIGHT',
|
||||
'GENERATE_ROOMNAMES_ON_WELCOME_PAGE',
|
||||
'INDICATOR_FONT_SIZES',
|
||||
'INITIAL_TOOLBAR_TIMEOUT',
|
||||
'LANG_DETECTION',
|
||||
'LOCAL_THUMBNAIL_RATIO',
|
||||
'MAXIMUM_ZOOMING_COEFFICIENT',
|
||||
'NATIVE_APP_NAME',
|
||||
'OPTIMAL_BROWSERS',
|
||||
'PHONE_NUMBER_REGEX',
|
||||
'PROVIDER_NAME',
|
||||
'RECENT_LIST_ENABLED',
|
||||
'REMOTE_THUMBNAIL_RATIO',
|
||||
'SETTINGS_SECTIONS',
|
||||
'SHARING_FEATURES',
|
||||
'SHOW_CHROME_EXTENSION_BANNER',
|
||||
'SHOW_POWERED_BY',
|
||||
'TILE_VIEW_MAX_COLUMNS',
|
||||
'TOOLBAR_ALWAYS_VISIBLE',
|
||||
'TOOLBAR_BUTTONS',
|
||||
'TOOLBAR_TIMEOUT',
|
||||
'UNSUPPORTED_BROWSERS',
|
||||
'VERTICAL_FILMSTRIP',
|
||||
'VIDEO_LAYOUT_FIT',
|
||||
'VIDEO_QUALITY_LABEL_DISABLED'
|
||||
].concat(extraInterfaceConfigWhitelistCopy).concat(isEmbedded() ? isEmbeddedInterfaceConfigWhitelist : []);
|
||||
10
react/features/base/config/isEmbeddedConfigWhitelist.ts
Normal file
10
react/features/base/config/isEmbeddedConfigWhitelist.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Additional config whitelist extending the original whitelist applied when Jitsi Meet is embedded
|
||||
* in another app be that with an iframe or a mobile SDK.
|
||||
*/
|
||||
export default [
|
||||
'customToolbarButtons',
|
||||
'defaultLogoUrl',
|
||||
'deploymentUrls',
|
||||
'liveStreaming'
|
||||
];
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Additional interface config whitelist extending the original whitelist applied when Jitsi Meet is embedded
|
||||
* in another app be that with an iframe or a mobile SDK.
|
||||
*/
|
||||
export default [
|
||||
];
|
||||
3
react/features/base/config/logger.ts
Normal file
3
react/features/base/config/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/config');
|
||||
225
react/features/base/config/middleware.ts
Normal file
225
react/features/base/config/middleware.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import { SET_DYNAMIC_BRANDING_DATA } from '../../dynamic-branding/actionTypes';
|
||||
import { setUserFilmstripWidth } from '../../filmstrip/actions.web';
|
||||
import { getFeatureFlag } from '../flags/functions';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import { updateSettings } from '../settings/actions';
|
||||
|
||||
import { OVERWRITE_CONFIG, SET_CONFIG } from './actionTypes';
|
||||
import { updateConfig } from './actions';
|
||||
import { IConfig } from './configType';
|
||||
|
||||
/**
|
||||
* The middleware of the feature {@code base/config}.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @private
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case SET_CONFIG:
|
||||
return _setConfig(store, next, action);
|
||||
|
||||
case SET_DYNAMIC_BRANDING_DATA:
|
||||
return _setDynamicBrandingData(store, next, action);
|
||||
|
||||
case OVERWRITE_CONFIG:
|
||||
return _updateSettings(store, next, action);
|
||||
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Notifies the feature {@code base/config} that the {@link SET_CONFIG} redux
|
||||
* action is being {@code dispatch}ed in a specific redux store.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} in the specified {@code store}.
|
||||
* @param {Action} action - The redux action which is being {@code dispatch}ed
|
||||
* in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {*} The return value of {@code next(action)}.
|
||||
*/
|
||||
function _setConfig({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
|
||||
// The reducer is doing some alterations to the config passed in the action,
|
||||
// so make sure it's the final state by waiting for the action to be
|
||||
// reduced.
|
||||
const result = next(action);
|
||||
const state = getState();
|
||||
|
||||
// Update the config with user defined settings.
|
||||
const settings = state['features/base/settings'];
|
||||
const config: IConfig = {};
|
||||
|
||||
if (typeof settings.disableP2P !== 'undefined') {
|
||||
config.p2p = { enabled: !settings.disableP2P };
|
||||
}
|
||||
|
||||
const resolutionFlag = getFeatureFlag(state, 'resolution');
|
||||
|
||||
if (typeof resolutionFlag !== 'undefined') {
|
||||
config.resolution = resolutionFlag;
|
||||
}
|
||||
|
||||
if (action.config.doNotFlipLocalVideo === true) {
|
||||
dispatch(updateSettings({
|
||||
localFlipX: false
|
||||
}));
|
||||
}
|
||||
|
||||
if (action.config.disableSelfView !== undefined) {
|
||||
dispatch(updateSettings({
|
||||
disableSelfView: action.config.disableSelfView
|
||||
}));
|
||||
}
|
||||
|
||||
const { initialWidth, stageFilmstripParticipants } = action.config.filmstrip || {};
|
||||
|
||||
if (stageFilmstripParticipants !== undefined) {
|
||||
dispatch(updateSettings({
|
||||
maxStageParticipants: stageFilmstripParticipants
|
||||
}));
|
||||
}
|
||||
|
||||
if (initialWidth) {
|
||||
dispatch(setUserFilmstripWidth(initialWidth));
|
||||
}
|
||||
|
||||
dispatch(updateConfig(config));
|
||||
|
||||
// FIXME On Web we rely on the global 'config' variable which gets altered
|
||||
// multiple times, before it makes it to the reducer. At some point it may
|
||||
// not be the global variable which is being modified anymore due to
|
||||
// different merge methods being used along the way. The global variable
|
||||
// must be synchronized with the final state resolved by the reducer.
|
||||
if (typeof window.config !== 'undefined') {
|
||||
window.config = state['features/base/config'];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates config based on dynamic branding data.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} in the specified {@code store}.
|
||||
* @param {Action} action - The redux action which is being {@code dispatch}ed
|
||||
* in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {*} The return value of {@code next(action)}.
|
||||
*/
|
||||
function _setDynamicBrandingData({ dispatch }: IStore, next: Function, action: AnyAction) {
|
||||
const config: IConfig = {};
|
||||
const {
|
||||
customParticipantMenuButtons,
|
||||
customToolbarButtons,
|
||||
downloadAppsUrl,
|
||||
etherpadBase,
|
||||
liveStreamingDialogUrls = {},
|
||||
preCallTest = {},
|
||||
salesforceUrl,
|
||||
userDocumentationUrl,
|
||||
peopleSearchUrl,
|
||||
} = action.value;
|
||||
|
||||
const { helpUrl, termsUrl, dataPrivacyUrl } = liveStreamingDialogUrls;
|
||||
|
||||
if (helpUrl || termsUrl || dataPrivacyUrl) {
|
||||
config.liveStreaming = {};
|
||||
if (helpUrl) {
|
||||
config.liveStreaming.helpLink = helpUrl;
|
||||
}
|
||||
|
||||
if (termsUrl) {
|
||||
config.liveStreaming.termsLink = termsUrl;
|
||||
}
|
||||
|
||||
if (dataPrivacyUrl) {
|
||||
config.liveStreaming.dataPrivacyLink = dataPrivacyUrl;
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadAppsUrl || userDocumentationUrl) {
|
||||
config.deploymentUrls = {};
|
||||
|
||||
if (downloadAppsUrl) {
|
||||
config.deploymentUrls.downloadAppsUrl = downloadAppsUrl;
|
||||
}
|
||||
|
||||
if (userDocumentationUrl) {
|
||||
config.deploymentUrls.userDocumentationURL = userDocumentationUrl;
|
||||
}
|
||||
}
|
||||
|
||||
if (salesforceUrl) {
|
||||
config.salesforceUrl = salesforceUrl;
|
||||
}
|
||||
|
||||
if (peopleSearchUrl) {
|
||||
config.peopleSearchUrl = peopleSearchUrl;
|
||||
}
|
||||
|
||||
const { enabled, iceUrl } = preCallTest;
|
||||
|
||||
if (typeof enabled === 'boolean') {
|
||||
config.prejoinConfig = {
|
||||
preCallTestEnabled: enabled
|
||||
};
|
||||
}
|
||||
|
||||
if (etherpadBase) {
|
||||
// eslint-disable-next-line camelcase
|
||||
config.etherpad_base = etherpadBase;
|
||||
}
|
||||
|
||||
if (iceUrl) {
|
||||
config.prejoinConfig = config.prejoinConfig || {};
|
||||
config.prejoinConfig.preCallTestICEUrl = iceUrl;
|
||||
}
|
||||
|
||||
if (customToolbarButtons) {
|
||||
config.customToolbarButtons = customToolbarButtons;
|
||||
}
|
||||
|
||||
if (customParticipantMenuButtons) {
|
||||
config.customParticipantMenuButtons = customParticipantMenuButtons;
|
||||
}
|
||||
|
||||
dispatch(updateConfig(config));
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates settings based on some config values.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} in the specified {@code store}.
|
||||
* @param {Action} action - The redux action which is being {@code dispatch}ed
|
||||
* in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {*} The return value of {@code next(action)}.
|
||||
*/
|
||||
function _updateSettings({ dispatch }: IStore, next: Function, action: AnyAction) {
|
||||
const { config: { doNotFlipLocalVideo } } = action;
|
||||
|
||||
if (doNotFlipLocalVideo === true) {
|
||||
dispatch(updateSettings({
|
||||
localFlipX: false
|
||||
}));
|
||||
}
|
||||
|
||||
return next(action);
|
||||
}
|
||||
592
react/features/base/config/reducer.ts
Normal file
592
react/features/base/config/reducer.ts
Normal file
@@ -0,0 +1,592 @@
|
||||
import { merge, union } from 'lodash-es';
|
||||
|
||||
import { CONFERENCE_INFO } from '../../conference/components/constants';
|
||||
import { TOOLBAR_BUTTONS } from '../../toolbox/constants';
|
||||
import { ToolbarButton } from '../../toolbox/types';
|
||||
import { CONNECTION_PROPERTIES_UPDATED } from '../connection/actionTypes';
|
||||
import ReducerRegistry from '../redux/ReducerRegistry';
|
||||
import { equals } from '../redux/functions';
|
||||
|
||||
import {
|
||||
CONFIG_WILL_LOAD,
|
||||
LOAD_CONFIG_ERROR,
|
||||
OVERWRITE_CONFIG,
|
||||
SET_CONFIG,
|
||||
UPDATE_CONFIG
|
||||
} from './actionTypes';
|
||||
import {
|
||||
IConfig,
|
||||
IDeeplinkingConfig,
|
||||
IDeeplinkingDesktopConfig,
|
||||
IDeeplinkingMobileConfig
|
||||
} from './configType';
|
||||
import { _cleanupConfig, _setDeeplinkingDefaults } from './functions';
|
||||
|
||||
/**
|
||||
* The initial state of the feature base/config when executing in a
|
||||
* non-React Native environment. The mandatory configuration to be passed to
|
||||
* JitsiMeetJS#init(). The app will download config.js from the Jitsi Meet
|
||||
* deployment and take its values into account but the values below will be
|
||||
* enforced (because they are essential to the correct execution of the
|
||||
* application).
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
const INITIAL_NON_RN_STATE: IConfig = {
|
||||
};
|
||||
|
||||
/**
|
||||
* The initial state of the feature base/config when executing in a React Native
|
||||
* environment. The mandatory configuration to be passed to JitsiMeetJS#init().
|
||||
* The app will download config.js from the Jitsi Meet deployment and take its
|
||||
* values into account but the values below will be enforced (because they are
|
||||
* essential to the correct execution of the application).
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
const INITIAL_RN_STATE: IConfig = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping between old configs controlling the conference info headers visibility and the
|
||||
* new configs. Needed in order to keep backwards compatibility.
|
||||
*/
|
||||
const CONFERENCE_HEADER_MAPPING = {
|
||||
hideConferenceTimer: [ 'conference-timer' ],
|
||||
hideConferenceSubject: [ 'subject' ],
|
||||
hideParticipantsStats: [ 'participants-count' ],
|
||||
hideRecordingLabel: [ 'recording' ]
|
||||
};
|
||||
|
||||
export interface IConfigState extends IConfig {
|
||||
analysis?: {
|
||||
obfuscateRoomName?: boolean;
|
||||
};
|
||||
error?: Error;
|
||||
oldConfig?: {
|
||||
bosh?: string;
|
||||
focusUserJid?: string;
|
||||
hosts: {
|
||||
domain: string;
|
||||
muc: string;
|
||||
};
|
||||
p2p?: object;
|
||||
websocket?: string;
|
||||
};
|
||||
}
|
||||
|
||||
ReducerRegistry.register<IConfigState>('features/base/config', (state = _getInitialState(), action): IConfigState => {
|
||||
switch (action.type) {
|
||||
case UPDATE_CONFIG:
|
||||
return _updateConfig(state, action);
|
||||
|
||||
case CONFIG_WILL_LOAD:
|
||||
return {
|
||||
error: undefined,
|
||||
|
||||
/**
|
||||
* The URL of the location associated with/configured by this
|
||||
* configuration.
|
||||
*
|
||||
* @type URL
|
||||
*/
|
||||
locationURL: action.locationURL
|
||||
};
|
||||
|
||||
case CONNECTION_PROPERTIES_UPDATED: {
|
||||
const { region, shard } = action.properties;
|
||||
const { deploymentInfo } = state;
|
||||
|
||||
if (deploymentInfo?.region === region && deploymentInfo?.shard === shard) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
deploymentInfo: JSON.parse(JSON.stringify({
|
||||
...deploymentInfo,
|
||||
region,
|
||||
shard
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
case LOAD_CONFIG_ERROR:
|
||||
// XXX LOAD_CONFIG_ERROR is one of the settlement execution paths of
|
||||
// the asynchronous "loadConfig procedure/process" started with
|
||||
// CONFIG_WILL_LOAD. Due to the asynchronous nature of it, whoever
|
||||
// is settling the process needs to provide proof that they have
|
||||
// started it and that the iteration of the process being completed
|
||||
// now is still of interest to the app.
|
||||
if (state.locationURL === action.locationURL) {
|
||||
return {
|
||||
/**
|
||||
* The {@link Error} which prevented the loading of the
|
||||
* configuration of the associated {@code locationURL}.
|
||||
*
|
||||
* @type Error
|
||||
*/
|
||||
error: action.error
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case SET_CONFIG:
|
||||
return _setConfig(state, action);
|
||||
|
||||
case OVERWRITE_CONFIG:
|
||||
return {
|
||||
...state,
|
||||
...action.config
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
/**
|
||||
* Gets the initial state of the feature base/config. The mandatory
|
||||
* configuration to be passed to JitsiMeetJS#init(). The app will download
|
||||
* config.js from the Jitsi Meet deployment and take its values into account but
|
||||
* the values below will be enforced (because they are essential to the correct
|
||||
* execution of the application).
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _getInitialState() {
|
||||
return (
|
||||
navigator.product === 'ReactNative'
|
||||
? INITIAL_RN_STATE
|
||||
: INITIAL_NON_RN_STATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a specific Redux action SET_CONFIG of the feature
|
||||
* base/lib-jitsi-meet.
|
||||
*
|
||||
* @param {IConfig} state - The Redux state of the feature base/config.
|
||||
* @param {Action} action - The Redux action SET_CONFIG to reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state after the reduction of the specified action.
|
||||
*/
|
||||
function _setConfig(state: IConfig, { config }: { config: IConfig; }) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
config = _translateLegacyConfig(config);
|
||||
|
||||
const { audioQuality } = config;
|
||||
const hdAudioOptions = {};
|
||||
|
||||
if (audioQuality?.stereo) {
|
||||
Object.assign(hdAudioOptions, {
|
||||
disableAP: true,
|
||||
enableNoAudioDetection: false,
|
||||
enableNoisyMicDetection: false,
|
||||
enableTalkWhileMuted: false
|
||||
});
|
||||
}
|
||||
|
||||
const { alwaysShowResizeBar, disableResizable } = config.filmstrip || {};
|
||||
|
||||
if (alwaysShowResizeBar && disableResizable) {
|
||||
config.filmstrip = {
|
||||
...config.filmstrip,
|
||||
alwaysShowResizeBar: false
|
||||
};
|
||||
}
|
||||
|
||||
const newState = merge(
|
||||
{},
|
||||
config,
|
||||
hdAudioOptions,
|
||||
{ error: undefined },
|
||||
|
||||
// The config of _getInitialState() is meant to override the config
|
||||
// downloaded from the Jitsi Meet deployment because the former contains
|
||||
// values that are mandatory.
|
||||
_getInitialState()
|
||||
);
|
||||
|
||||
_cleanupConfig(newState);
|
||||
|
||||
return equals(state, newState) ? state : newState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the conferenceInfo object against the defaults.
|
||||
*
|
||||
* @param {IConfig} config - The old config.
|
||||
* @returns {Object} The processed conferenceInfo object.
|
||||
*/
|
||||
function _getConferenceInfo(config: IConfig) {
|
||||
const { conferenceInfo } = config;
|
||||
|
||||
if (conferenceInfo) {
|
||||
return {
|
||||
alwaysVisible: conferenceInfo.alwaysVisible ?? [ ...CONFERENCE_INFO.alwaysVisible ],
|
||||
autoHide: conferenceInfo.autoHide ?? [ ...CONFERENCE_INFO.autoHide ]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...CONFERENCE_INFO
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new config {@code Object}, if necessary, out of a specific
|
||||
* interface_config {@code Object} which is in the latest format supported by jitsi-meet.
|
||||
*
|
||||
* @param {Object} oldValue - The config {@code Object} which may or may not be
|
||||
* in the latest form supported by jitsi-meet and from which a new config
|
||||
* {@code Object} is to be constructed if necessary.
|
||||
* @returns {Object} A config {@code Object} which is in the latest format
|
||||
* supported by jitsi-meet.
|
||||
*/
|
||||
function _translateInterfaceConfig(oldValue: IConfig) {
|
||||
const newValue = oldValue;
|
||||
|
||||
if (!Array.isArray(oldValue.toolbarButtons)
|
||||
&& typeof interfaceConfig === 'object' && Array.isArray(interfaceConfig.TOOLBAR_BUTTONS)) {
|
||||
newValue.toolbarButtons = interfaceConfig.TOOLBAR_BUTTONS;
|
||||
}
|
||||
|
||||
if (!oldValue.toolbarConfig) {
|
||||
oldValue.toolbarConfig = {};
|
||||
}
|
||||
|
||||
newValue.toolbarConfig = oldValue.toolbarConfig || {};
|
||||
if (typeof oldValue.toolbarConfig.alwaysVisible !== 'boolean'
|
||||
&& typeof interfaceConfig === 'object'
|
||||
&& typeof interfaceConfig.TOOLBAR_ALWAYS_VISIBLE === 'boolean') {
|
||||
newValue.toolbarConfig.alwaysVisible = interfaceConfig.TOOLBAR_ALWAYS_VISIBLE;
|
||||
}
|
||||
|
||||
if (typeof oldValue.toolbarConfig.initialTimeout !== 'number'
|
||||
&& typeof interfaceConfig === 'object'
|
||||
&& typeof interfaceConfig.INITIAL_TOOLBAR_TIMEOUT === 'number') {
|
||||
newValue.toolbarConfig.initialTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT;
|
||||
}
|
||||
|
||||
if (typeof oldValue.toolbarConfig.timeout !== 'number'
|
||||
&& typeof interfaceConfig === 'object'
|
||||
&& typeof interfaceConfig.TOOLBAR_TIMEOUT === 'number') {
|
||||
newValue.toolbarConfig.timeout = interfaceConfig.TOOLBAR_TIMEOUT;
|
||||
}
|
||||
|
||||
if (!oldValue.connectionIndicators
|
||||
&& typeof interfaceConfig === 'object'
|
||||
&& (interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_DISABLED')
|
||||
|| interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_AUTO_HIDE_ENABLED')
|
||||
|| interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT'))) {
|
||||
newValue.connectionIndicators = {
|
||||
disabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
|
||||
autoHide: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
|
||||
autoHideTimeout: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT
|
||||
};
|
||||
}
|
||||
|
||||
if (oldValue.disableModeratorIndicator === undefined
|
||||
&& typeof interfaceConfig === 'object'
|
||||
&& interfaceConfig.hasOwnProperty('DISABLE_FOCUS_INDICATOR')) {
|
||||
newValue.disableModeratorIndicator = interfaceConfig.DISABLE_FOCUS_INDICATOR;
|
||||
}
|
||||
|
||||
if (oldValue.defaultLocalDisplayName === undefined
|
||||
&& typeof interfaceConfig === 'object'
|
||||
&& interfaceConfig.hasOwnProperty('DEFAULT_LOCAL_DISPLAY_NAME')) {
|
||||
newValue.defaultLocalDisplayName = interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME;
|
||||
}
|
||||
|
||||
if (oldValue.defaultRemoteDisplayName === undefined
|
||||
&& typeof interfaceConfig === 'object'
|
||||
&& interfaceConfig.hasOwnProperty('DEFAULT_REMOTE_DISPLAY_NAME')) {
|
||||
newValue.defaultRemoteDisplayName = interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
|
||||
}
|
||||
|
||||
if (oldValue.defaultLogoUrl === undefined) {
|
||||
if (typeof interfaceConfig === 'object'
|
||||
&& interfaceConfig.hasOwnProperty('DEFAULT_LOGO_URL')) {
|
||||
newValue.defaultLogoUrl = interfaceConfig.DEFAULT_LOGO_URL;
|
||||
} else {
|
||||
newValue.defaultLogoUrl = 'images/watermark.svg';
|
||||
}
|
||||
}
|
||||
|
||||
// if we have `deeplinking` defined, ignore deprecated values, except `disableDeepLinking`.
|
||||
// Otherwise, compose the config.
|
||||
if (oldValue.deeplinking && newValue.deeplinking) { // make TS happy
|
||||
newValue.deeplinking.disabled = oldValue.deeplinking.hasOwnProperty('disabled')
|
||||
? oldValue.deeplinking.disabled
|
||||
: Boolean(oldValue.disableDeepLinking);
|
||||
} else {
|
||||
const disabled = Boolean(oldValue.disableDeepLinking);
|
||||
const deeplinking: IDeeplinkingConfig = {
|
||||
desktop: {} as IDeeplinkingDesktopConfig,
|
||||
hideLogo: false,
|
||||
disabled,
|
||||
android: {} as IDeeplinkingMobileConfig,
|
||||
ios: {} as IDeeplinkingMobileConfig
|
||||
};
|
||||
|
||||
if (typeof interfaceConfig === 'object') {
|
||||
if (deeplinking.desktop) {
|
||||
deeplinking.desktop.appName = interfaceConfig.NATIVE_APP_NAME;
|
||||
}
|
||||
|
||||
deeplinking.hideLogo = Boolean(interfaceConfig.HIDE_DEEP_LINKING_LOGO);
|
||||
deeplinking.android = {
|
||||
appName: interfaceConfig.NATIVE_APP_NAME,
|
||||
appScheme: interfaceConfig.APP_SCHEME,
|
||||
downloadLink: interfaceConfig.MOBILE_DOWNLOAD_LINK_ANDROID,
|
||||
appPackage: interfaceConfig.ANDROID_APP_PACKAGE,
|
||||
fDroidUrl: interfaceConfig.MOBILE_DOWNLOAD_LINK_F_DROID
|
||||
};
|
||||
deeplinking.ios = {
|
||||
appName: interfaceConfig.NATIVE_APP_NAME,
|
||||
appScheme: interfaceConfig.APP_SCHEME,
|
||||
downloadLink: interfaceConfig.MOBILE_DOWNLOAD_LINK_IOS
|
||||
};
|
||||
}
|
||||
newValue.deeplinking = deeplinking;
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new config {@code Object}, if necessary, out of a specific
|
||||
* config {@code Object} which is in the latest format supported by jitsi-meet.
|
||||
* Such a translation from an old config format to a new/the latest config
|
||||
* format is necessary because the mobile app bundles jitsi-meet and
|
||||
* lib-jitsi-meet at build time and does not download them at runtime from the
|
||||
* deployment on which it will join a conference.
|
||||
*
|
||||
* @param {Object} oldValue - The config {@code Object} which may or may not be
|
||||
* in the latest form supported by jitsi-meet and from which a new config
|
||||
* {@code Object} is to be constructed if necessary.
|
||||
* @returns {Object} A config {@code Object} which is in the latest format
|
||||
* supported by jitsi-meet.
|
||||
*/
|
||||
function _translateLegacyConfig(oldValue: IConfig) {
|
||||
const newValue = _translateInterfaceConfig(oldValue);
|
||||
|
||||
// Translate deprecated config values to new config values.
|
||||
|
||||
const filteredConferenceInfo = Object.keys(CONFERENCE_HEADER_MAPPING).filter(key => oldValue[key as keyof IConfig]);
|
||||
|
||||
if (filteredConferenceInfo.length) {
|
||||
newValue.conferenceInfo = _getConferenceInfo(oldValue);
|
||||
|
||||
filteredConferenceInfo.forEach(key => {
|
||||
newValue.conferenceInfo = oldValue.conferenceInfo ?? {};
|
||||
|
||||
// hideRecordingLabel does not mean not render it at all, but autoHide it
|
||||
if (key === 'hideRecordingLabel') {
|
||||
newValue.conferenceInfo.alwaysVisible
|
||||
= (newValue.conferenceInfo?.alwaysVisible ?? [])
|
||||
.filter(c => !CONFERENCE_HEADER_MAPPING[key].includes(c));
|
||||
newValue.conferenceInfo.autoHide
|
||||
= union(newValue.conferenceInfo.autoHide, CONFERENCE_HEADER_MAPPING[key]);
|
||||
} else {
|
||||
newValue.conferenceInfo.alwaysVisible
|
||||
= (newValue.conferenceInfo.alwaysVisible ?? [])
|
||||
.filter(c => !CONFERENCE_HEADER_MAPPING[key as keyof typeof CONFERENCE_HEADER_MAPPING].includes(c));
|
||||
newValue.conferenceInfo.autoHide
|
||||
= (newValue.conferenceInfo.autoHide ?? []).filter(c =>
|
||||
!CONFERENCE_HEADER_MAPPING[key as keyof typeof CONFERENCE_HEADER_MAPPING].includes(c));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
newValue.welcomePage = oldValue.welcomePage || {};
|
||||
if (oldValue.hasOwnProperty('enableWelcomePage')
|
||||
&& !newValue.welcomePage.hasOwnProperty('disabled')
|
||||
) {
|
||||
newValue.welcomePage.disabled = !oldValue.enableWelcomePage;
|
||||
}
|
||||
|
||||
newValue.disabledSounds = newValue.disabledSounds || [];
|
||||
|
||||
if (oldValue.disableJoinLeaveSounds) {
|
||||
newValue.disabledSounds.unshift('PARTICIPANT_LEFT_SOUND', 'PARTICIPANT_JOINED_SOUND');
|
||||
}
|
||||
|
||||
if (oldValue.disableRecordAudioNotification) {
|
||||
newValue.disabledSounds.unshift(
|
||||
'RECORDING_ON_SOUND',
|
||||
'RECORDING_OFF_SOUND',
|
||||
'LIVE_STREAMING_ON_SOUND',
|
||||
'LIVE_STREAMING_OFF_SOUND'
|
||||
);
|
||||
}
|
||||
|
||||
if (oldValue.disableIncomingMessageSound) {
|
||||
newValue.disabledSounds.unshift('INCOMING_MSG_SOUND');
|
||||
}
|
||||
|
||||
newValue.raisedHands = newValue.raisedHands || {};
|
||||
|
||||
if (oldValue.disableRemoveRaisedHandOnFocus) {
|
||||
newValue.raisedHands.disableRemoveRaisedHandOnFocus = oldValue.disableRemoveRaisedHandOnFocus;
|
||||
}
|
||||
|
||||
if (oldValue.stereo || oldValue.opusMaxAverageBitrate) {
|
||||
newValue.audioQuality = {
|
||||
opusMaxAverageBitrate: oldValue.audioQuality?.opusMaxAverageBitrate ?? oldValue.opusMaxAverageBitrate,
|
||||
stereo: oldValue.audioQuality?.stereo ?? oldValue.stereo
|
||||
};
|
||||
}
|
||||
|
||||
newValue.e2ee = newValue.e2ee || {};
|
||||
|
||||
if (oldValue.e2eeLabels) {
|
||||
newValue.e2ee.labels = oldValue.e2eeLabels;
|
||||
}
|
||||
|
||||
newValue.defaultLocalDisplayName
|
||||
= newValue.defaultLocalDisplayName || 'me';
|
||||
|
||||
if (oldValue.hideAddRoomButton) {
|
||||
newValue.breakoutRooms = {
|
||||
/* eslint-disable-next-line no-extra-parens */
|
||||
...(newValue.breakoutRooms || {}),
|
||||
hideAddRoomButton: oldValue.hideAddRoomButton
|
||||
};
|
||||
}
|
||||
|
||||
newValue.defaultRemoteDisplayName
|
||||
= newValue.defaultRemoteDisplayName || 'Fellow Jitster';
|
||||
|
||||
newValue.transcription = newValue.transcription || {};
|
||||
if (oldValue.transcribingEnabled !== undefined) {
|
||||
newValue.transcription = {
|
||||
...newValue.transcription,
|
||||
enabled: oldValue.transcribingEnabled
|
||||
};
|
||||
}
|
||||
if (oldValue.transcribeWithAppLanguage !== undefined) {
|
||||
newValue.transcription = {
|
||||
...newValue.transcription,
|
||||
useAppLanguage: oldValue.transcribeWithAppLanguage
|
||||
};
|
||||
}
|
||||
if (oldValue.preferredTranscribeLanguage !== undefined) {
|
||||
newValue.transcription = {
|
||||
...newValue.transcription,
|
||||
preferredLanguage: oldValue.preferredTranscribeLanguage
|
||||
};
|
||||
}
|
||||
if (oldValue.autoCaptionOnRecord !== undefined) {
|
||||
newValue.transcription = {
|
||||
...newValue.transcription,
|
||||
autoTranscribeOnRecord: oldValue.autoCaptionOnRecord
|
||||
};
|
||||
}
|
||||
|
||||
newValue.recordingService = newValue.recordingService || {};
|
||||
if (oldValue.fileRecordingsServiceEnabled !== undefined
|
||||
&& newValue.recordingService.enabled === undefined) {
|
||||
newValue.recordingService = {
|
||||
...newValue.recordingService,
|
||||
enabled: oldValue.fileRecordingsServiceEnabled
|
||||
};
|
||||
}
|
||||
if (oldValue.fileRecordingsServiceSharingEnabled !== undefined
|
||||
&& newValue.recordingService.sharingEnabled === undefined) {
|
||||
newValue.recordingService = {
|
||||
...newValue.recordingService,
|
||||
sharingEnabled: oldValue.fileRecordingsServiceSharingEnabled
|
||||
};
|
||||
}
|
||||
|
||||
newValue.liveStreaming = newValue.liveStreaming || {};
|
||||
|
||||
// Migrate config.liveStreamingEnabled
|
||||
if (oldValue.liveStreamingEnabled !== undefined) {
|
||||
newValue.liveStreaming = {
|
||||
...newValue.liveStreaming,
|
||||
enabled: oldValue.liveStreamingEnabled
|
||||
};
|
||||
}
|
||||
|
||||
// Migrate interfaceConfig.LIVE_STREAMING_HELP_LINK
|
||||
if (oldValue.liveStreaming === undefined
|
||||
&& typeof interfaceConfig === 'object'
|
||||
&& interfaceConfig.hasOwnProperty('LIVE_STREAMING_HELP_LINK')) {
|
||||
newValue.liveStreaming = {
|
||||
...newValue.liveStreaming,
|
||||
helpLink: interfaceConfig.LIVE_STREAMING_HELP_LINK
|
||||
};
|
||||
}
|
||||
|
||||
newValue.speakerStats = newValue.speakerStats || {};
|
||||
|
||||
if (oldValue.disableSpeakerStatsSearch !== undefined
|
||||
&& newValue.speakerStats.disableSearch === undefined
|
||||
) {
|
||||
newValue.speakerStats = {
|
||||
...newValue.speakerStats,
|
||||
disableSearch: oldValue.disableSpeakerStatsSearch
|
||||
};
|
||||
}
|
||||
|
||||
if (oldValue.speakerStatsOrder !== undefined
|
||||
&& newValue.speakerStats.order === undefined) {
|
||||
newValue.speakerStats = {
|
||||
...newValue.speakerStats,
|
||||
order: oldValue.speakerStatsOrder
|
||||
};
|
||||
}
|
||||
|
||||
if (oldValue.autoKnockLobby !== undefined
|
||||
&& newValue.lobby?.autoKnock === undefined) {
|
||||
newValue.lobby = {
|
||||
...newValue.lobby || {},
|
||||
autoKnock: oldValue.autoKnockLobby
|
||||
};
|
||||
}
|
||||
|
||||
if (oldValue.enableLobbyChat !== undefined
|
||||
&& newValue.lobby?.enableChat === undefined) {
|
||||
newValue.lobby = {
|
||||
...newValue.lobby || {},
|
||||
enableChat: oldValue.enableLobbyChat
|
||||
};
|
||||
}
|
||||
|
||||
if (oldValue.hideLobbyButton !== undefined
|
||||
&& newValue.securityUi?.hideLobbyButton === undefined) {
|
||||
newValue.securityUi = {
|
||||
...newValue.securityUi || {},
|
||||
hideLobbyButton: oldValue.hideLobbyButton
|
||||
};
|
||||
}
|
||||
|
||||
// Profile button is not available on mobile
|
||||
if (navigator.product !== 'ReactNative') {
|
||||
if (oldValue.disableProfile) {
|
||||
newValue.toolbarButtons = (newValue.toolbarButtons || TOOLBAR_BUTTONS)
|
||||
.filter((button: ToolbarButton) => button !== 'profile');
|
||||
}
|
||||
}
|
||||
|
||||
_setDeeplinkingDefaults(newValue.deeplinking as IDeeplinkingConfig);
|
||||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the stored configuration with the given extra options.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature base/config.
|
||||
* @param {Action} action - The Redux action to reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state after the reduction of the specified action.
|
||||
*/
|
||||
function _updateConfig(state: IConfig, { config }: { config: IConfig; }) {
|
||||
const newState = merge({}, state, config);
|
||||
|
||||
_cleanupConfig(newState);
|
||||
|
||||
return equals(state, newState) ? state : newState;
|
||||
}
|
||||
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 '';
|
||||
}
|
||||
96
react/features/base/devices/actionTypes.ts
Normal file
96
react/features/base/devices/actionTypes.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* The type of Redux action which signals that an error occurred while obtaining
|
||||
* a camera.
|
||||
*
|
||||
* {
|
||||
* type: NOTIFY_CAMERA_ERROR,
|
||||
* error: Object
|
||||
* }
|
||||
*/
|
||||
export const NOTIFY_CAMERA_ERROR = 'NOTIFY_CAMERA_ERROR';
|
||||
|
||||
/**
|
||||
* The type of Redux action which signals that an error occurred while obtaining
|
||||
* a microphone.
|
||||
*
|
||||
* {
|
||||
* type: NOTIFY_MIC_ERROR,
|
||||
* error: Object
|
||||
* }
|
||||
*/
|
||||
export const NOTIFY_MIC_ERROR = 'NOTIFY_MIC_ERROR';
|
||||
|
||||
/**
|
||||
* The type of Redux action which signals that the currently used audio
|
||||
* input device should be changed.
|
||||
*
|
||||
* {
|
||||
* type: SET_AUDIO_INPUT_DEVICE,
|
||||
* deviceId: string,
|
||||
* }
|
||||
*/
|
||||
export const SET_AUDIO_INPUT_DEVICE = 'SET_AUDIO_INPUT_DEVICE';
|
||||
|
||||
/**
|
||||
* The type of Redux action which signals that the currently used video
|
||||
* input device should be changed.
|
||||
*
|
||||
* {
|
||||
* type: SET_VIDEO_INPUT_DEVICE,
|
||||
* deviceId: string,
|
||||
* }
|
||||
*/
|
||||
export const SET_VIDEO_INPUT_DEVICE = 'SET_VIDEO_INPUT_DEVICE';
|
||||
|
||||
/**
|
||||
* The type of Redux action which signals that the list of known available
|
||||
* audio and video sources has changed.
|
||||
*
|
||||
* {
|
||||
* type: UPDATE_DEVICE_LIST,
|
||||
* devices: Array<MediaDeviceInfo>,
|
||||
* }
|
||||
*/
|
||||
export const UPDATE_DEVICE_LIST = 'UPDATE_DEVICE_LIST';
|
||||
|
||||
/**
|
||||
* The type of Redux action which will add a pending device requests that will
|
||||
* be executed later when it is possible (when the conference is joined).
|
||||
*
|
||||
* {
|
||||
* type: ADD_PENDING_DEVICE_REQUEST,
|
||||
* request: Object
|
||||
* }
|
||||
*/
|
||||
export const ADD_PENDING_DEVICE_REQUEST = 'ADD_PENDING_DEVICE_REQUEST';
|
||||
|
||||
/**
|
||||
* The type of Redux action which will remove all pending device requests.
|
||||
*
|
||||
* {
|
||||
* type: REMOVE_PENDING_DEVICE_REQUESTS
|
||||
* }
|
||||
*/
|
||||
export const REMOVE_PENDING_DEVICE_REQUESTS = 'REMOVE_PENDING_DEVICE_REQUESTS';
|
||||
|
||||
/**
|
||||
* The type of Redux action which will check passed old and passed new devices
|
||||
* and if needed will show notifications asking the user whether to use those.
|
||||
*
|
||||
* {
|
||||
* type: CHECK_AND_NOTIFY_FOR_NEW_DEVICE
|
||||
* newDevices: Array<MediaDeviceInfo>
|
||||
* oldDevices: Array<MediaDeviceInfo>
|
||||
* }
|
||||
*/
|
||||
export const CHECK_AND_NOTIFY_FOR_NEW_DEVICE = 'CHECK_AND_NOTIFY_FOR_NEW_DEVICE';
|
||||
|
||||
/**
|
||||
* The type of Redux action which signals that the device permissions have changed.
|
||||
*
|
||||
* {
|
||||
* type: CHECK_AND_NOTIFY_FOR_NEW_DEVICE
|
||||
* permissions: Object
|
||||
* }
|
||||
*/
|
||||
export const DEVICE_PERMISSIONS_CHANGED = 'DEVICE_PERMISSIONS_CHANGED';
|
||||
356
react/features/base/devices/actions.web.ts
Normal file
356
react/features/base/devices/actions.web.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
import { IStore } from '../../app/types';
|
||||
import JitsiMeetJS from '../lib-jitsi-meet';
|
||||
import { updateSettings } from '../settings/actions';
|
||||
import { getUserSelectedOutputDeviceId } from '../settings/functions.web';
|
||||
|
||||
import {
|
||||
ADD_PENDING_DEVICE_REQUEST,
|
||||
CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
|
||||
DEVICE_PERMISSIONS_CHANGED,
|
||||
NOTIFY_CAMERA_ERROR,
|
||||
NOTIFY_MIC_ERROR,
|
||||
REMOVE_PENDING_DEVICE_REQUESTS,
|
||||
SET_AUDIO_INPUT_DEVICE,
|
||||
SET_VIDEO_INPUT_DEVICE,
|
||||
UPDATE_DEVICE_LIST
|
||||
} from './actionTypes';
|
||||
import {
|
||||
areDeviceLabelsInitialized,
|
||||
areDevicesDifferent,
|
||||
filterIgnoredDevices,
|
||||
flattenAvailableDevices,
|
||||
getDeviceIdByLabel,
|
||||
getDeviceLabelById,
|
||||
getDevicesFromURL,
|
||||
logDevices,
|
||||
setAudioOutputDeviceId
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Maps the WebRTC string for device type to the keys used to store configure,
|
||||
* within redux, which devices should be used by default.
|
||||
*/
|
||||
const DEVICE_TYPE_TO_SETTINGS_KEYS = {
|
||||
audioInput: {
|
||||
currentDeviceId: 'micDeviceId',
|
||||
userSelectedDeviceId: 'userSelectedMicDeviceId',
|
||||
userSelectedDeviceLabel: 'userSelectedMicDeviceLabel'
|
||||
},
|
||||
audioOutput: {
|
||||
currentDeviceId: 'audioOutputDeviceId',
|
||||
userSelectedDeviceId: 'userSelectedAudioOutputDeviceId',
|
||||
userSelectedDeviceLabel: 'userSelectedAudioOutputDeviceLabel'
|
||||
},
|
||||
videoInput: {
|
||||
currentDeviceId: 'cameraDeviceId',
|
||||
userSelectedDeviceId: 'userSelectedCameraDeviceId',
|
||||
userSelectedDeviceLabel: 'userSelectedCameraDeviceLabel'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a pending device request.
|
||||
*
|
||||
* @param {Object} request - The request to be added.
|
||||
* @returns {{
|
||||
* type: ADD_PENDING_DEVICE_REQUEST,
|
||||
* request: Object
|
||||
* }}
|
||||
*/
|
||||
export function addPendingDeviceRequest(request: Object) {
|
||||
return {
|
||||
type: ADD_PENDING_DEVICE_REQUEST,
|
||||
request
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the initial A/V devices before the conference has started.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function configureInitialDevices() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const deviceLabels = getDevicesFromURL(getState());
|
||||
let updateSettingsPromise;
|
||||
|
||||
logger.debug(`(TIME) configureInitialDevices: deviceLabels=${
|
||||
Boolean(deviceLabels)}, performance.now=${window.performance.now()}`);
|
||||
|
||||
if (deviceLabels) {
|
||||
updateSettingsPromise = dispatch(getAvailableDevices()).then(() => {
|
||||
const state = getState();
|
||||
|
||||
if (!areDeviceLabelsInitialized(state)) {
|
||||
// The labels are not available if the A/V permissions are
|
||||
// not yet granted.
|
||||
|
||||
Object.keys(deviceLabels).forEach(key => {
|
||||
dispatch(addPendingDeviceRequest({
|
||||
type: 'devices',
|
||||
name: 'setDevice',
|
||||
device: {
|
||||
kind: key.toLowerCase(),
|
||||
label: deviceLabels[key as keyof typeof deviceLabels]
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
responseCallback() {}
|
||||
}));
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const newSettings: any = {};
|
||||
|
||||
Object.keys(deviceLabels).forEach(key => {
|
||||
const label = deviceLabels[key as keyof typeof deviceLabels];
|
||||
|
||||
// @ts-ignore
|
||||
const deviceId = getDeviceIdByLabel(state, label, key);
|
||||
|
||||
if (deviceId) {
|
||||
const settingsTranslationMap = DEVICE_TYPE_TO_SETTINGS_KEYS[
|
||||
key as keyof typeof DEVICE_TYPE_TO_SETTINGS_KEYS];
|
||||
|
||||
newSettings[settingsTranslationMap.currentDeviceId] = deviceId;
|
||||
newSettings[settingsTranslationMap.userSelectedDeviceId] = deviceId;
|
||||
newSettings[settingsTranslationMap.userSelectedDeviceLabel] = label;
|
||||
}
|
||||
});
|
||||
|
||||
dispatch(updateSettings(newSettings));
|
||||
});
|
||||
} else {
|
||||
updateSettingsPromise = Promise.resolve();
|
||||
}
|
||||
|
||||
return updateSettingsPromise
|
||||
.then(() => {
|
||||
const userSelectedAudioOutputDeviceId = getUserSelectedOutputDeviceId(getState());
|
||||
|
||||
logger.debug(`(TIME) configureInitialDevices -> setAudioOutputDeviceId: performance.now=${
|
||||
window.performance.now()}`);
|
||||
|
||||
return setAudioOutputDeviceId(userSelectedAudioOutputDeviceId, dispatch)
|
||||
.catch(ex => logger.warn(`Failed to set audio output device.
|
||||
Default audio output device will be used instead ${ex}`));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries for connected A/V input and output devices and updates the redux
|
||||
* state of known devices.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function getAvailableDevices() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => new Promise(resolve => {
|
||||
const { mediaDevices } = JitsiMeetJS;
|
||||
|
||||
if (mediaDevices.isDeviceChangeAvailable()) {
|
||||
mediaDevices.enumerateDevices((devices: MediaDeviceInfo[]) => {
|
||||
const { filteredDevices, ignoredDevices } = filterIgnoredDevices(devices);
|
||||
const oldDevices = flattenAvailableDevices(getState()['features/base/devices'].availableDevices);
|
||||
|
||||
if (areDevicesDifferent(oldDevices, filteredDevices)) {
|
||||
logDevices(ignoredDevices, 'Ignored devices on device list changed:');
|
||||
dispatch(updateDeviceList(filteredDevices));
|
||||
}
|
||||
|
||||
resolve(filteredDevices);
|
||||
});
|
||||
} else {
|
||||
resolve([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that an error occurred while trying to obtain a track from a camera.
|
||||
*
|
||||
* @param {Object} error - The device error, as provided by lib-jitsi-meet.
|
||||
* @param {string} error.name - The constant for the type of the error.
|
||||
* @param {string} error.message - Optional additional information about the
|
||||
* error.
|
||||
* @returns {{
|
||||
* type: NOTIFY_CAMERA_ERROR,
|
||||
* error: Object
|
||||
* }}
|
||||
*/
|
||||
export function notifyCameraError(error: Error) {
|
||||
return {
|
||||
type: NOTIFY_CAMERA_ERROR,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that an error occurred while trying to obtain a track from a mic.
|
||||
*
|
||||
* @param {Object} error - The device error, as provided by lib-jitsi-meet.
|
||||
* @param {Object} error.name - The constant for the type of the error.
|
||||
* @param {string} error.message - Optional additional information about the
|
||||
* error.
|
||||
* @returns {{
|
||||
* type: NOTIFY_MIC_ERROR,
|
||||
* error: Object
|
||||
* }}
|
||||
*/
|
||||
export function notifyMicError(error: Error) {
|
||||
return {
|
||||
type: NOTIFY_MIC_ERROR,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all pending device requests.
|
||||
*
|
||||
* @returns {{
|
||||
* type: REMOVE_PENDING_DEVICE_REQUESTS
|
||||
* }}
|
||||
*/
|
||||
export function removePendingDeviceRequests() {
|
||||
return {
|
||||
type: REMOVE_PENDING_DEVICE_REQUESTS
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals to update the currently used audio input device.
|
||||
*
|
||||
* @param {string} deviceId - The id of the new audio input device.
|
||||
* @returns {{
|
||||
* type: SET_AUDIO_INPUT_DEVICE,
|
||||
* deviceId: string
|
||||
* }}
|
||||
*/
|
||||
export function setAudioInputDevice(deviceId: string) {
|
||||
return {
|
||||
type: SET_AUDIO_INPUT_DEVICE,
|
||||
deviceId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio input device id and updates the settings
|
||||
* so they are persisted across sessions.
|
||||
*
|
||||
* @param {string} deviceId - The id of the new audio input device.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setAudioInputDeviceAndUpdateSettings(deviceId: string) {
|
||||
return function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
const deviceLabel = getDeviceLabelById(getState(), deviceId, 'audioInput');
|
||||
|
||||
dispatch(setAudioInputDevice(deviceId));
|
||||
dispatch(updateSettings({
|
||||
userSelectedMicDeviceId: deviceId,
|
||||
userSelectedMicDeviceLabel: deviceLabel
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the output device id.
|
||||
*
|
||||
* @param {string} deviceId - The id of the new output device.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setAudioOutputDevice(deviceId: string) {
|
||||
return function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
const deviceLabel = getDeviceLabelById(getState(), deviceId, 'audioOutput');
|
||||
|
||||
return setAudioOutputDeviceId(deviceId, dispatch, true, deviceLabel);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals to update the currently used video input device.
|
||||
*
|
||||
* @param {string} deviceId - The id of the new video input device.
|
||||
* @returns {{
|
||||
* type: SET_VIDEO_INPUT_DEVICE,
|
||||
* deviceId: string
|
||||
* }}
|
||||
*/
|
||||
export function setVideoInputDevice(deviceId: string) {
|
||||
return {
|
||||
type: SET_VIDEO_INPUT_DEVICE,
|
||||
deviceId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the video input device id and updates the settings
|
||||
* so they are persisted across sessions.
|
||||
*
|
||||
* @param {string} deviceId - The id of the new video input device.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setVideoInputDeviceAndUpdateSettings(deviceId: string) {
|
||||
return function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
const deviceLabel = getDeviceLabelById(getState(), deviceId, 'videoInput');
|
||||
|
||||
dispatch(setVideoInputDevice(deviceId));
|
||||
dispatch(updateSettings({
|
||||
userSelectedCameraDeviceId: deviceId,
|
||||
userSelectedCameraDeviceLabel: deviceLabel
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals to update the list of known audio and video devices.
|
||||
*
|
||||
* @param {Array<MediaDeviceInfo>} devices - All known available audio input,
|
||||
* audio output, and video input devices.
|
||||
* @returns {{
|
||||
* type: UPDATE_DEVICE_LIST,
|
||||
* devices: Array<MediaDeviceInfo>
|
||||
* }}
|
||||
*/
|
||||
export function updateDeviceList(devices: MediaDeviceInfo[]) {
|
||||
return {
|
||||
type: UPDATE_DEVICE_LIST,
|
||||
devices
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals to check new and old devices for newly added devices and notify.
|
||||
*
|
||||
* @param {Array<MediaDeviceInfo>} newDevices - Array of the new devices.
|
||||
* @param {Array<MediaDeviceInfo>} oldDevices - Array of the old devices.
|
||||
* @returns {{
|
||||
* type: CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
|
||||
* newDevices: Array<MediaDeviceInfo>,
|
||||
* oldDevices: Array<MediaDeviceInfo>
|
||||
* }}
|
||||
*/
|
||||
export function checkAndNotifyForNewDevice(newDevices: MediaDeviceInfo[], oldDevices: MediaDeviceInfo[]) {
|
||||
return {
|
||||
type: CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
|
||||
newDevices,
|
||||
oldDevices
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the device permissions have changed.
|
||||
*
|
||||
* @param {Object} permissions - Object with the permissions.
|
||||
* @returns {{
|
||||
* type: DEVICE_PERMISSIONS_CHANGED,
|
||||
* permissions: Object
|
||||
* }}
|
||||
*/
|
||||
export function devicePermissionsChanged(permissions: Object) {
|
||||
return {
|
||||
type: DEVICE_PERMISSIONS_CHANGED,
|
||||
permissions
|
||||
};
|
||||
}
|
||||
11
react/features/base/devices/constants.ts
Normal file
11
react/features/base/devices/constants.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Prefixes of devices that will be filtered from the device list.
|
||||
*
|
||||
* NOTE: It seems that the filtered devices can't be set
|
||||
* as default device on the OS level and this use case is not handled in the code. If we add more device prefixes that
|
||||
* can be default devices we should make sure to handle the default device use case.
|
||||
*/
|
||||
export const DEVICE_LABEL_PREFIXES_TO_IGNORE = [
|
||||
'Microsoft Teams Audio Device',
|
||||
'ZoomAudioDevice'
|
||||
];
|
||||
19
react/features/base/devices/functions.any.ts
Normal file
19
react/features/base/devices/functions.any.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { IReduxState } from '../../app/types';
|
||||
|
||||
/**
|
||||
* Returns true if there are devices of a specific type or on native platform.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @param {string} type - The type of device: VideoOutput | audioOutput | audioInput.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function hasAvailableDevices(state: IReduxState, type: string) {
|
||||
if (state['features/base/devices'] === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const availableDevices = state['features/base/devices'].availableDevices;
|
||||
|
||||
return Number(availableDevices[type as keyof typeof availableDevices]?.length) > 0;
|
||||
}
|
||||
1
react/features/base/devices/functions.native.ts
Normal file
1
react/features/base/devices/functions.native.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './functions.any';
|
||||
383
react/features/base/devices/functions.web.ts
Normal file
383
react/features/base/devices/functions.web.ts
Normal file
@@ -0,0 +1,383 @@
|
||||
/* eslint-disable require-jsdoc */
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import JitsiMeetJS from '../lib-jitsi-meet';
|
||||
import { updateSettings } from '../settings/actions';
|
||||
import { ISettingsState } from '../settings/reducer';
|
||||
import { setNewAudioOutputDevice } from '../sounds/functions.web';
|
||||
import { parseURLParams } from '../util/parseURLParams';
|
||||
|
||||
import { DEVICE_LABEL_PREFIXES_TO_IGNORE } from './constants';
|
||||
import logger from './logger';
|
||||
import { IDevicesState } from './types';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
const webrtcKindToJitsiKindTranslator = {
|
||||
audioinput: 'audioInput',
|
||||
audiooutput: 'audioOutput',
|
||||
videoinput: 'videoInput'
|
||||
};
|
||||
|
||||
/**
|
||||
* Detects the use case when the labels are not available if the A/V permissions
|
||||
* are not yet granted.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {boolean} - True if the labels are already initialized and false
|
||||
* otherwise.
|
||||
*/
|
||||
export function areDeviceLabelsInitialized(state: IReduxState) {
|
||||
// TODO: Replace with something that doesn't use APP when the conference.js logic is reactified.
|
||||
if (APP.conference._localTracksInitialized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const type of [ 'audioInput', 'audioOutput', 'videoInput' ]) {
|
||||
const availableDevices = state['features/base/devices'].availableDevices;
|
||||
|
||||
if ((availableDevices[type as keyof typeof availableDevices] || []).find(d => Boolean(d.label))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device id of the audio output device which is currently in use.
|
||||
* Empty string stands for default device.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getAudioOutputDeviceId() {
|
||||
return JitsiMeetJS.mediaDevices.getAudioOutputDevice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the real device id of the default device of the given type.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {*} kind - The type of the device. One of "audioInput",
|
||||
* "audioOutput", and "videoInput". Also supported is all lowercase versions
|
||||
* of the preceding types.
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
export function getDefaultDeviceId(state: IReduxState, kind: string) {
|
||||
const kindToSearch = webrtcKindToJitsiKindTranslator[kind as keyof typeof webrtcKindToJitsiKindTranslator] || kind;
|
||||
const availableDevices = state['features/base/devices'].availableDevices;
|
||||
const defaultDevice = (availableDevices[kindToSearch as keyof typeof availableDevices] || [])
|
||||
.find(d => d.deviceId === 'default');
|
||||
|
||||
// Find the device with a matching group id.
|
||||
const matchingDevice = (availableDevices[kindToSearch as keyof typeof availableDevices] || [])
|
||||
.find(d => d.deviceId !== 'default' && d.groupId === defaultDevice?.groupId);
|
||||
|
||||
if (matchingDevice) {
|
||||
return matchingDevice.deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a device with a label that matches the passed label and returns its id.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {string} label - The label.
|
||||
* @param {string} kind - The type of the device. One of "audioInput",
|
||||
* "audioOutput", and "videoInput". Also supported is all lowercase versions
|
||||
* of the preceding types.
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
export function getDeviceIdByLabel(state: IReduxState, label: string, kind: string) {
|
||||
const kindToSearch = webrtcKindToJitsiKindTranslator[kind as keyof typeof webrtcKindToJitsiKindTranslator] || kind;
|
||||
|
||||
const availableDevices = state['features/base/devices'].availableDevices;
|
||||
const device
|
||||
= (availableDevices[kindToSearch as keyof typeof availableDevices] || [])
|
||||
.find(d => d.label === label);
|
||||
|
||||
if (device) {
|
||||
return device.deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a device with a label that matches the passed id and returns its label.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {string} id - The device id.
|
||||
* @param {string} kind - The type of the device. One of "audioInput",
|
||||
* "audioOutput", and "videoInput". Also supported is all lowercase versions
|
||||
* of the preceding types.
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
export function getDeviceLabelById(state: IReduxState, id: string, kind: string) {
|
||||
const kindToSearch = webrtcKindToJitsiKindTranslator[kind as keyof typeof webrtcKindToJitsiKindTranslator] || kind;
|
||||
|
||||
const availableDevices = state['features/base/devices'].availableDevices;
|
||||
const device
|
||||
= (availableDevices[kindToSearch as keyof typeof availableDevices] || [])
|
||||
.find(d => d.deviceId === id);
|
||||
|
||||
if (device) {
|
||||
return device.label;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the devices set in the URL.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Object|undefined}
|
||||
*/
|
||||
export function getDevicesFromURL(state: IReduxState) {
|
||||
const urlParams
|
||||
= parseURLParams(state['features/base/connection'].locationURL ?? '');
|
||||
|
||||
const audioOutput = urlParams['devices.audioOutput'];
|
||||
const videoInput = urlParams['devices.videoInput'];
|
||||
const audioInput = urlParams['devices.audioInput'];
|
||||
|
||||
if (!audioOutput && !videoInput && !audioInput) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const devices: IDevicesState['availableDevices'] = {};
|
||||
|
||||
audioOutput && (devices.audioOutput = audioOutput);
|
||||
videoInput && (devices.videoInput = videoInput);
|
||||
audioInput && (devices.audioInput = audioInput);
|
||||
|
||||
return devices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array of media devices into an object organized by device kind.
|
||||
*
|
||||
* @param {Array<MediaDeviceInfo>} devices - Available media devices.
|
||||
* @private
|
||||
* @returns {Object} An object with the media devices split by type. The keys
|
||||
* are device type and the values are arrays with devices matching the device
|
||||
* type.
|
||||
*/
|
||||
// @ts-ignore
|
||||
export function groupDevicesByKind(devices: MediaDeviceInfo[]): IDevicesState['availableDevices'] {
|
||||
return {
|
||||
audioInput: devices.filter(device => device.kind === 'audioinput'),
|
||||
audioOutput: devices.filter(device => device.kind === 'audiooutput'),
|
||||
videoInput: devices.filter(device => device.kind === 'videoinput')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the devices that start with one of the prefixes from DEVICE_LABEL_PREFIXES_TO_IGNORE.
|
||||
*
|
||||
* @param {MediaDeviceInfo[]} devices - The devices to be filtered.
|
||||
* @returns {MediaDeviceInfo[]} - The filtered devices.
|
||||
*/
|
||||
// @ts-ignore
|
||||
export function filterIgnoredDevices(devices: MediaDeviceInfo[] = []) {
|
||||
|
||||
// @ts-ignore
|
||||
const ignoredDevices: MediaDeviceInfo[] = [];
|
||||
const filteredDevices = devices.filter(device => {
|
||||
if (!device.label) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (DEVICE_LABEL_PREFIXES_TO_IGNORE.find(prefix => device.label?.startsWith(prefix))) {
|
||||
ignoredDevices.push(device);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return {
|
||||
filteredDevices,
|
||||
ignoredDevices
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the passed device arrays are different.
|
||||
*
|
||||
* @param {MediaDeviceInfo[]} devices1 - Array with devices to be compared.
|
||||
* @param {MediaDeviceInfo[]} devices2 - Array with devices to be compared.
|
||||
* @returns {boolean} - True if the device arrays are different and false otherwise.
|
||||
*/
|
||||
// @ts-ignore
|
||||
export function areDevicesDifferent(devices1: MediaDeviceInfo[] = [], devices2: MediaDeviceInfo[] = []) {
|
||||
if (devices1.length !== devices2.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let i = 0; i < devices1.length; i++) {
|
||||
const device1 = devices1[i];
|
||||
const found = devices2.find(({ deviceId, groupId, kind, label }) =>
|
||||
device1.deviceId === deviceId
|
||||
&& device1.groupId === groupId
|
||||
&& device1.kind === kind
|
||||
&& device1.label === label
|
||||
);
|
||||
|
||||
if (!found) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens the availableDevices from redux.
|
||||
*
|
||||
* @param {IDevicesState.availableDevices} devices - The available devices from redux.
|
||||
* @returns {MediaDeviceInfo[]} - The flattened array of devices.
|
||||
*/
|
||||
export function flattenAvailableDevices(
|
||||
{ audioInput = [], audioOutput = [], videoInput = [] }: IDevicesState['availableDevices']) {
|
||||
return audioInput.concat(audioOutput).concat(videoInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* We want to strip any device details that are not very user friendly, like usb ids put in brackets at the end.
|
||||
*
|
||||
* @param {string} label - Device label to format.
|
||||
*
|
||||
* @returns {string} - Formatted string.
|
||||
*/
|
||||
export function formatDeviceLabel(label: string) {
|
||||
|
||||
let formattedLabel = label;
|
||||
|
||||
// Remove braked description at the end as it contains non user friendly strings i.e.
|
||||
// Microsoft® LifeCam HD-3000 (045e:0779:31dg:d1231)
|
||||
const ix = formattedLabel.lastIndexOf('(');
|
||||
|
||||
if (ix !== -1) {
|
||||
formattedLabel = formattedLabel.substr(0, ix);
|
||||
}
|
||||
|
||||
return formattedLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of objects containing all the microphone device ids and labels.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
export function getAudioInputDeviceData(state: IReduxState) {
|
||||
return state['features/base/devices'].availableDevices.audioInput?.map(
|
||||
({ deviceId, label }) => {
|
||||
return {
|
||||
deviceId,
|
||||
label
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of objectes containing all the output device ids and labels.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
export function getAudioOutputDeviceData(state: IReduxState) {
|
||||
return state['features/base/devices'].availableDevices.audioOutput?.map(
|
||||
({ deviceId, label }) => {
|
||||
return {
|
||||
deviceId,
|
||||
label
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all the camera device ids.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function getVideoDeviceIds(state: IReduxState) {
|
||||
return state['features/base/devices'].availableDevices.videoInput?.map(({ deviceId }) => deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array of device info objects into string.
|
||||
*
|
||||
* @param {MediaDeviceInfo[]} devices - The devices.
|
||||
* @returns {string}
|
||||
*/
|
||||
// @ts-ignore
|
||||
function devicesToStr(devices?: MediaDeviceInfo[]) {
|
||||
return devices?.map(device => `\t\t${device.label}[${device.deviceId}]`).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an array of devices.
|
||||
*
|
||||
* @param {MediaDeviceInfo[]} devices - The array of devices.
|
||||
* @param {string} title - The title that will be printed in the log.
|
||||
* @returns {void}
|
||||
*/
|
||||
// @ts-ignore
|
||||
export function logDevices(devices: MediaDeviceInfo[], title = '') {
|
||||
const deviceList = groupDevicesByKind(devices);
|
||||
const audioInputs = devicesToStr(deviceList.audioInput);
|
||||
const audioOutputs = devicesToStr(deviceList.audioOutput);
|
||||
const videoInputs = devicesToStr(deviceList.videoInput);
|
||||
|
||||
logger.debug(`${title}:\n`
|
||||
+ `audioInput:\n${audioInputs}\n`
|
||||
+ `audioOutput:\n${audioOutputs}\n`
|
||||
+ `videoInput:\n${videoInputs}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set device id of the audio output device which is currently in use.
|
||||
* Empty string stands for default device.
|
||||
*
|
||||
* @param {string} newId - New audio output device id.
|
||||
* @param {Function} dispatch - The Redux dispatch function.
|
||||
* @param {boolean} userSelection - Whether this is a user selection update.
|
||||
* @param {?string} newLabel - New audio output device label to store.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function setAudioOutputDeviceId(
|
||||
newId = 'default',
|
||||
dispatch: IStore['dispatch'],
|
||||
userSelection = false,
|
||||
newLabel?: string): Promise<any> {
|
||||
|
||||
logger.debug(`setAudioOutputDevice: ${String(newLabel)}[${newId}]`);
|
||||
|
||||
if (!JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output')) {
|
||||
logger.warn('Adjusting audio output is not supported');
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return JitsiMeetJS.mediaDevices.setAudioOutputDevice(newId)
|
||||
.then(() => {
|
||||
dispatch(setNewAudioOutputDevice(newId));
|
||||
const newSettings: Partial<ISettingsState> = {
|
||||
audioOutputDeviceId: newId,
|
||||
userSelectedAudioOutputDeviceId: undefined,
|
||||
userSelectedAudioOutputDeviceLabel: undefined
|
||||
};
|
||||
|
||||
if (userSelection) {
|
||||
newSettings.userSelectedAudioOutputDeviceId = newId;
|
||||
newSettings.userSelectedAudioOutputDeviceLabel = newLabel;
|
||||
} else {
|
||||
// a flow workaround, I needed to add 'userSelectedAudioOutputDeviceId: undefined'
|
||||
delete newSettings.userSelectedAudioOutputDeviceId;
|
||||
delete newSettings.userSelectedAudioOutputDeviceLabel;
|
||||
}
|
||||
|
||||
return dispatch(updateSettings(newSettings));
|
||||
});
|
||||
}
|
||||
3
react/features/base/devices/logger.ts
Normal file
3
react/features/base/devices/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/devices');
|
||||
349
react/features/base/devices/middleware.web.ts
Normal file
349
react/features/base/devices/middleware.web.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import { processExternalDeviceRequest } from '../../device-selection/functions';
|
||||
import { showNotification, showWarningNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { replaceAudioTrackById, replaceVideoTrackById, setDeviceStatusWarning } from '../../prejoin/actions';
|
||||
import { isPrejoinPageVisible } from '../../prejoin/functions';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app/actionTypes';
|
||||
import { isMobileBrowser } from '../environment/utils';
|
||||
import JitsiMeetJS, { JitsiMediaDevicesEvents, JitsiTrackErrors } from '../lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../media/constants';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import { updateSettings } from '../settings/actions';
|
||||
import { getLocalTrack } from '../tracks/functions';
|
||||
|
||||
import {
|
||||
CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
|
||||
NOTIFY_CAMERA_ERROR,
|
||||
NOTIFY_MIC_ERROR,
|
||||
SET_AUDIO_INPUT_DEVICE,
|
||||
SET_VIDEO_INPUT_DEVICE,
|
||||
UPDATE_DEVICE_LIST
|
||||
} from './actionTypes';
|
||||
import {
|
||||
devicePermissionsChanged,
|
||||
removePendingDeviceRequests,
|
||||
setAudioInputDevice,
|
||||
setVideoInputDevice
|
||||
} from './actions';
|
||||
import {
|
||||
areDeviceLabelsInitialized,
|
||||
formatDeviceLabel,
|
||||
logDevices,
|
||||
setAudioOutputDeviceId
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
|
||||
microphone: {
|
||||
[JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.micConstraintFailedError',
|
||||
[JitsiTrackErrors.GENERAL]: 'dialog.micUnknownError',
|
||||
[JitsiTrackErrors.NOT_FOUND]: 'dialog.micNotFoundError',
|
||||
[JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.micPermissionDeniedError',
|
||||
[JitsiTrackErrors.TIMEOUT]: 'dialog.micTimeoutError'
|
||||
},
|
||||
camera: {
|
||||
[JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.cameraConstraintFailedError',
|
||||
[JitsiTrackErrors.GENERAL]: 'dialog.cameraUnknownError',
|
||||
[JitsiTrackErrors.NOT_FOUND]: 'dialog.cameraNotFoundError',
|
||||
[JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.cameraPermissionDeniedError',
|
||||
[JitsiTrackErrors.UNSUPPORTED_RESOLUTION]: 'dialog.cameraUnsupportedResolutionError',
|
||||
[JitsiTrackErrors.TIMEOUT]: 'dialog.cameraTimeoutError'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A listener for device permissions changed reported from lib-jitsi-meet.
|
||||
*/
|
||||
let permissionsListener: Function | undefined;
|
||||
|
||||
/**
|
||||
* Implements the middleware of the feature base/devices.
|
||||
*
|
||||
* @param {Store} store - Redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT: {
|
||||
const _permissionsListener = (permissions: Object) => {
|
||||
store.dispatch(devicePermissionsChanged(permissions));
|
||||
};
|
||||
const { mediaDevices } = JitsiMeetJS;
|
||||
|
||||
permissionsListener = _permissionsListener;
|
||||
mediaDevices.addEventListener(JitsiMediaDevicesEvents.PERMISSIONS_CHANGED, permissionsListener);
|
||||
Promise.all([
|
||||
mediaDevices.isDevicePermissionGranted('audio'),
|
||||
mediaDevices.isDevicePermissionGranted('video')
|
||||
])
|
||||
.then(results => {
|
||||
_permissionsListener({
|
||||
audio: results[0],
|
||||
video: results[1]
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
break;
|
||||
}
|
||||
case APP_WILL_UNMOUNT:
|
||||
if (typeof permissionsListener === 'function') {
|
||||
JitsiMeetJS.mediaDevices.removeEventListener(
|
||||
JitsiMediaDevicesEvents.PERMISSIONS_CHANGED, permissionsListener);
|
||||
permissionsListener = undefined;
|
||||
}
|
||||
break;
|
||||
case NOTIFY_CAMERA_ERROR: {
|
||||
if (!action.error) {
|
||||
break;
|
||||
}
|
||||
|
||||
const { message, name } = action.error;
|
||||
|
||||
const cameraJitsiTrackErrorMsg
|
||||
= JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[name];
|
||||
const cameraErrorMsg = cameraJitsiTrackErrorMsg
|
||||
|| JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.camera[JitsiTrackErrors.GENERAL];
|
||||
const additionalCameraErrorMsg = cameraJitsiTrackErrorMsg ? null : message;
|
||||
const titleKey = name === JitsiTrackErrors.PERMISSION_DENIED
|
||||
? 'deviceError.cameraPermission' : 'deviceError.cameraError';
|
||||
|
||||
store.dispatch(showWarningNotification({
|
||||
description: additionalCameraErrorMsg,
|
||||
descriptionKey: cameraErrorMsg,
|
||||
titleKey
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
store.dispatch(setDeviceStatusWarning(titleKey));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case NOTIFY_MIC_ERROR: {
|
||||
if (!action.error) {
|
||||
break;
|
||||
}
|
||||
|
||||
const { message, name } = action.error;
|
||||
|
||||
const micJitsiTrackErrorMsg
|
||||
= JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[name];
|
||||
const micErrorMsg = micJitsiTrackErrorMsg
|
||||
|| JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.microphone[JitsiTrackErrors.GENERAL];
|
||||
const additionalMicErrorMsg = micJitsiTrackErrorMsg ? null : message;
|
||||
const titleKey = name === JitsiTrackErrors.PERMISSION_DENIED
|
||||
? 'deviceError.microphonePermission'
|
||||
: 'deviceError.microphoneError';
|
||||
|
||||
store.dispatch(showWarningNotification({
|
||||
description: additionalMicErrorMsg,
|
||||
descriptionKey: micErrorMsg,
|
||||
titleKey
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
store.dispatch(setDeviceStatusWarning(titleKey));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case SET_AUDIO_INPUT_DEVICE:
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
store.dispatch(replaceAudioTrackById(action.deviceId));
|
||||
} else {
|
||||
APP.conference.onAudioDeviceChanged(action.deviceId);
|
||||
}
|
||||
break;
|
||||
case SET_VIDEO_INPUT_DEVICE: {
|
||||
const localTrack = getLocalTrack(store.getState()['features/base/tracks'], MEDIA_TYPE.VIDEO);
|
||||
|
||||
// on mobile devices the video stream has to be stopped before replacing it
|
||||
if (isMobileBrowser() && localTrack && !localTrack.muted) {
|
||||
localTrack.jitsiTrack.stopStream();
|
||||
}
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
store.dispatch(replaceVideoTrackById(action.deviceId));
|
||||
} else {
|
||||
APP.conference.onVideoDeviceChanged(action.deviceId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case UPDATE_DEVICE_LIST:
|
||||
logDevices(action.devices, 'Device list updated');
|
||||
if (areDeviceLabelsInitialized(store.getState())) {
|
||||
return _processPendingRequests(store, next, action);
|
||||
}
|
||||
break;
|
||||
case CHECK_AND_NOTIFY_FOR_NEW_DEVICE:
|
||||
_checkAndNotifyForNewDevice(store, action.newDevices, action.oldDevices);
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Does extra sync up on properties that may need to be updated after the
|
||||
* conference was joined.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} to the specified {@code store}.
|
||||
* @param {Action} action - The redux action {@code CONFERENCE_JOINED} which is
|
||||
* being dispatched in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _processPendingRequests({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
|
||||
const result = next(action);
|
||||
const state = getState();
|
||||
const { pendingRequests } = state['features/base/devices'];
|
||||
|
||||
if (!pendingRequests || pendingRequests.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
pendingRequests.forEach((request: any) => {
|
||||
processExternalDeviceRequest(
|
||||
dispatch,
|
||||
getState,
|
||||
request,
|
||||
request.responseCallback);
|
||||
});
|
||||
dispatch(removePendingDeviceRequests());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a new device by comparing new and old array of devices and dispatches
|
||||
* notification with the new device. For new devices with same groupId only one
|
||||
* notification will be shown, this is so to avoid showing multiple notifications
|
||||
* for audio input and audio output devices.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {MediaDeviceInfo[]} newDevices - The array of new devices we received.
|
||||
* @param {MediaDeviceInfo[]} oldDevices - The array of the old devices we have.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _checkAndNotifyForNewDevice(store: IStore, newDevices: MediaDeviceInfo[], oldDevices: MediaDeviceInfo[]) {
|
||||
const { dispatch } = store;
|
||||
|
||||
// let's intersect both newDevices and oldDevices and handle thew newly
|
||||
// added devices
|
||||
const onlyNewDevices = newDevices.filter(
|
||||
nDevice => !oldDevices.find(
|
||||
device => device.deviceId === nDevice.deviceId));
|
||||
|
||||
// we group devices by groupID which normally is the grouping by physical device
|
||||
// plugging in headset we provide normally two device, one input and one output
|
||||
// and we want to show only one notification for this physical audio device
|
||||
const devicesGroupBy: {
|
||||
[key: string]: MediaDeviceInfo[];
|
||||
} = onlyNewDevices.reduce((accumulated: any, value) => {
|
||||
accumulated[value.groupId] = accumulated[value.groupId] || [];
|
||||
accumulated[value.groupId].push(value);
|
||||
|
||||
return accumulated;
|
||||
}, {});
|
||||
|
||||
Object.values(devicesGroupBy).forEach(devicesArray => {
|
||||
|
||||
if (devicesArray.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// let's get the first device as a reference, we will use it for
|
||||
// label and type
|
||||
const newDevice = devicesArray[0];
|
||||
|
||||
// we want to strip any device details that are not very
|
||||
// user friendly, like usb ids put in brackets at the end
|
||||
const description = formatDeviceLabel(newDevice.label);
|
||||
|
||||
let titleKey;
|
||||
|
||||
switch (newDevice.kind) {
|
||||
case 'videoinput': {
|
||||
titleKey = 'notify.newDeviceCameraTitle';
|
||||
break;
|
||||
}
|
||||
case 'audioinput' :
|
||||
case 'audiooutput': {
|
||||
titleKey = 'notify.newDeviceAudioTitle';
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isPrejoinPageVisible(store.getState())) {
|
||||
dispatch(showNotification({
|
||||
description,
|
||||
titleKey,
|
||||
customActionNameKey: [ 'notify.newDeviceAction' ],
|
||||
customActionHandler: [ _useDevice.bind(undefined, store, devicesArray) ]
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a device to be currently used, selected by the user.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Array<MediaDeviceInfo|InputDeviceInfo>} devices - The devices to save.
|
||||
* @returns {boolean} - Returns true in order notifications to be dismissed.
|
||||
* @private
|
||||
*/
|
||||
function _useDevice({ dispatch }: IStore, devices: MediaDeviceInfo[]) {
|
||||
devices.forEach(device => {
|
||||
switch (device.kind) {
|
||||
case 'videoinput': {
|
||||
dispatch(updateSettings({
|
||||
userSelectedCameraDeviceId: device.deviceId,
|
||||
userSelectedCameraDeviceLabel: device.label
|
||||
}));
|
||||
|
||||
dispatch(setVideoInputDevice(device.deviceId));
|
||||
break;
|
||||
}
|
||||
case 'audioinput': {
|
||||
dispatch(updateSettings({
|
||||
userSelectedMicDeviceId: device.deviceId,
|
||||
userSelectedMicDeviceLabel: device.label
|
||||
}));
|
||||
|
||||
dispatch(setAudioInputDevice(device.deviceId));
|
||||
break;
|
||||
}
|
||||
case 'audiooutput': {
|
||||
setAudioOutputDeviceId(
|
||||
device.deviceId,
|
||||
dispatch,
|
||||
true,
|
||||
device.label)
|
||||
.then(() => logger.log('changed audio output device'))
|
||||
.catch(err => {
|
||||
logger.warn(
|
||||
'Failed to change audio output device.',
|
||||
'Default or previously set audio output device will',
|
||||
' be used instead.',
|
||||
err);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
88
react/features/base/devices/reducer.web.ts
Normal file
88
react/features/base/devices/reducer.web.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import ReducerRegistry from '../redux/ReducerRegistry';
|
||||
|
||||
import {
|
||||
ADD_PENDING_DEVICE_REQUEST,
|
||||
DEVICE_PERMISSIONS_CHANGED,
|
||||
REMOVE_PENDING_DEVICE_REQUESTS,
|
||||
SET_AUDIO_INPUT_DEVICE,
|
||||
SET_VIDEO_INPUT_DEVICE,
|
||||
UPDATE_DEVICE_LIST
|
||||
} from './actionTypes';
|
||||
import { groupDevicesByKind } from './functions.web';
|
||||
import logger from './logger';
|
||||
import { IDevicesState } from './types';
|
||||
|
||||
|
||||
const DEFAULT_STATE: IDevicesState = {
|
||||
availableDevices: {
|
||||
audioInput: [],
|
||||
audioOutput: [],
|
||||
videoInput: []
|
||||
},
|
||||
pendingRequests: [],
|
||||
permissions: {
|
||||
audio: false,
|
||||
video: false
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen for actions which changes the state of known and used devices.
|
||||
*
|
||||
* @param {IDevicesState} state - The Redux state of the feature features/base/devices.
|
||||
* @param {Object} action - Action object.
|
||||
* @param {string} action.type - Type of action.
|
||||
* @param {Array<MediaDeviceInfo>} action.devices - All available audio and
|
||||
* video devices.
|
||||
* @returns {Object}
|
||||
*/
|
||||
ReducerRegistry.register<IDevicesState>(
|
||||
'features/base/devices',
|
||||
(state = DEFAULT_STATE, action): IDevicesState => {
|
||||
switch (action.type) {
|
||||
case UPDATE_DEVICE_LIST: {
|
||||
const deviceList = groupDevicesByKind(action.devices);
|
||||
|
||||
return {
|
||||
...state,
|
||||
availableDevices: deviceList
|
||||
};
|
||||
}
|
||||
|
||||
case ADD_PENDING_DEVICE_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
pendingRequests: [
|
||||
...state.pendingRequests,
|
||||
action.request
|
||||
]
|
||||
};
|
||||
|
||||
case REMOVE_PENDING_DEVICE_REQUESTS:
|
||||
return {
|
||||
...state,
|
||||
pendingRequests: [ ]
|
||||
};
|
||||
|
||||
// TODO: Changing of current audio and video device id is currently handled outside of react/redux.
|
||||
case SET_AUDIO_INPUT_DEVICE: {
|
||||
logger.debug(`set audio input device: ${action.deviceId}`);
|
||||
|
||||
return state;
|
||||
}
|
||||
case SET_VIDEO_INPUT_DEVICE: {
|
||||
logger.debug(`set video input device: ${action.deviceId}`);
|
||||
|
||||
return state;
|
||||
}
|
||||
case DEVICE_PERMISSIONS_CHANGED: {
|
||||
return {
|
||||
...state,
|
||||
permissions: action.permissions
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
||||
|
||||
17
react/features/base/devices/types.ts
Normal file
17
react/features/base/devices/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
|
||||
export interface IDevicesState {
|
||||
availableDevices: {
|
||||
// @ts-ignore
|
||||
audioInput?: MediaDeviceInfo[];
|
||||
// @ts-ignore
|
||||
audioOutput?: MediaDeviceInfo[];
|
||||
// @ts-ignore
|
||||
videoInput?: MediaDeviceInfo[];
|
||||
};
|
||||
pendingRequests: any[];
|
||||
permissions: {
|
||||
audio: boolean;
|
||||
video: boolean;
|
||||
};
|
||||
}
|
||||
41
react/features/base/dialog/actionTypes.ts
Normal file
41
react/features/base/dialog/actionTypes.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* The type of Redux action which closes a dialog
|
||||
*
|
||||
* {
|
||||
* type: HIDE_DIALOG
|
||||
* }
|
||||
*/
|
||||
export const HIDE_DIALOG = 'HIDE_DIALOG';
|
||||
|
||||
/**
|
||||
* The type of Redux action which closes a sheet.
|
||||
*
|
||||
* {
|
||||
* type: HIDE_SHEET
|
||||
* }
|
||||
*/
|
||||
export const HIDE_SHEET = 'HIDE_SHEET';
|
||||
|
||||
/**
|
||||
* The type of Redux action which begins a request to open a dialog.
|
||||
*
|
||||
* {
|
||||
* type: OPEN_DIALOG,
|
||||
* component: React.Component,
|
||||
* props: PropTypes
|
||||
* }
|
||||
*
|
||||
*/
|
||||
export const OPEN_DIALOG = 'OPEN_DIALOG';
|
||||
|
||||
/**
|
||||
* The type of Redux action which begins a request to open a sheet.
|
||||
*
|
||||
* {
|
||||
* type: OPEN_SHEET,
|
||||
* component: React.Component,
|
||||
* props: PropTypes
|
||||
* }
|
||||
*
|
||||
*/
|
||||
export const OPEN_SHEET = 'OPEN_SHEET';
|
||||
126
react/features/base/dialog/actions.ts
Normal file
126
react/features/base/dialog/actions.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
|
||||
import {
|
||||
HIDE_DIALOG,
|
||||
HIDE_SHEET,
|
||||
OPEN_DIALOG,
|
||||
OPEN_SHEET
|
||||
} from './actionTypes';
|
||||
import { isDialogOpen } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Signals Dialog to close its dialog.
|
||||
*
|
||||
* @param {Object} [component] - The {@code Dialog} component to close/hide. If
|
||||
* {@code undefined}, closes/hides {@code Dialog} regardless of which
|
||||
* component it's rendering; otherwise, closes/hides {@code Dialog} only if
|
||||
* it's rendering the specified {@code component}.
|
||||
* @returns {{
|
||||
* type: HIDE_DIALOG,
|
||||
* component: (React.Component | undefined)
|
||||
* }}
|
||||
*/
|
||||
export function hideDialog(component?: ComponentType<any>) {
|
||||
logger.info(`Hide dialog: ${getComponentDisplayName(component)}`);
|
||||
|
||||
return {
|
||||
type: HIDE_DIALOG,
|
||||
component
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the active sheet.
|
||||
*
|
||||
* @returns {{
|
||||
* type: HIDE_SHEET,
|
||||
* }}
|
||||
*/
|
||||
export function hideSheet() {
|
||||
return {
|
||||
type: HIDE_SHEET
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals Dialog to open dialog.
|
||||
*
|
||||
* @param {Object} component - The component to display as dialog.
|
||||
* @param {Object} [componentProps] - The React {@code Component} props of the
|
||||
* specified {@code component}.
|
||||
* @returns {{
|
||||
* type: OPEN_DIALOG,
|
||||
* component: React.Component,
|
||||
* componentProps: (Object | undefined)
|
||||
* }}
|
||||
*/
|
||||
export function openDialog(component: ComponentType<any>, componentProps?: Object) {
|
||||
logger.info(`Open dialog: ${getComponentDisplayName(component)}`);
|
||||
|
||||
return {
|
||||
type: OPEN_DIALOG,
|
||||
component,
|
||||
componentProps
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the requested sheet.
|
||||
*
|
||||
* @param {Object} component - The component to display as a sheet.
|
||||
* @param {Object} [componentProps] - The React {@code Component} props of the
|
||||
* specified {@code component}.
|
||||
* @returns {{
|
||||
* type: OPEN_SHEET,
|
||||
* component: React.Component,
|
||||
* componentProps: (Object | undefined)
|
||||
* }}
|
||||
*/
|
||||
export function openSheet(component: ComponentType<any>, componentProps?: Object) {
|
||||
return {
|
||||
type: OPEN_SHEET,
|
||||
component,
|
||||
componentProps
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals Dialog to open a dialog with the specified component if the component
|
||||
* is not already open. If it is open, then Dialog is signaled to close its
|
||||
* dialog.
|
||||
*
|
||||
* @param {Object} component - The component to display as dialog.
|
||||
* @param {Object} [componentProps] - The React {@code Component} props of the
|
||||
* specified {@code component}.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function toggleDialog(component: ComponentType<any>, componentProps?: Object) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
if (isDialogOpen(getState, component)) {
|
||||
dispatch(hideDialog(component));
|
||||
} else {
|
||||
dispatch(openDialog(component, componentProps));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a printable name for a dialog component.
|
||||
*
|
||||
* @param {Object} component - The component to extract the name for.
|
||||
*
|
||||
* @returns {string} The display name.
|
||||
*/
|
||||
function getComponentDisplayName(component?: ComponentType<any>) {
|
||||
if (!component) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const name = component.displayName ?? component.name ?? 'Component';
|
||||
|
||||
return name.replace('withI18nextTranslation(Connect(', '') // dialogs with translations
|
||||
.replace('))', ''); // dialogs with translations suffix
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import React, { Component, ComponentType } from 'react';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { IReactionEmojiProps } from '../../../reactions/constants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link DialogContainer}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The component to render.
|
||||
*/
|
||||
_component?: ComponentType<any>;
|
||||
|
||||
/**
|
||||
* The props to pass to the component that will be rendered.
|
||||
*/
|
||||
_componentProps?: Object;
|
||||
|
||||
/**
|
||||
* Array of reactions to be displayed.
|
||||
*/
|
||||
_reactionsQueue: Array<IReactionEmojiProps>;
|
||||
|
||||
/**
|
||||
* True if the UI is in a compact state where we don't show dialogs.
|
||||
*/
|
||||
_reducedUI: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a DialogContainer responsible for showing all dialogs.
|
||||
*/
|
||||
export default class AbstractDialogContainer extends Component<IProps> {
|
||||
/**
|
||||
* Returns the dialog to be displayed.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement|null}
|
||||
*/
|
||||
_renderDialogContent() {
|
||||
const {
|
||||
_component: component,
|
||||
_reducedUI: reducedUI
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
component && !reducedUI
|
||||
? React.createElement(component, this.props._componentProps)
|
||||
: null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated
|
||||
* {@code AbstractDialogContainer}'s props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
export function abstractMapStateToProps(state: IReduxState) {
|
||||
const stateFeaturesBaseDialog = state['features/base/dialog'];
|
||||
const { reducedUI } = state['features/base/responsive-ui'];
|
||||
|
||||
return {
|
||||
_component: stateFeaturesBaseDialog.component,
|
||||
_componentProps: stateFeaturesBaseDialog.componentProps,
|
||||
_reducedUI: reducedUI
|
||||
};
|
||||
}
|
||||
57
react/features/base/dialog/components/functions.native.tsx
Normal file
57
react/features/base/dialog/components/functions.native.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { Text, TextStyle } from 'react-native';
|
||||
|
||||
import { brandedDialog as styles } from './native/styles';
|
||||
|
||||
/**
|
||||
* Renders a specific {@code string} which may contain HTML.
|
||||
*
|
||||
* @param {string|undefined} html - The {@code string} which may
|
||||
* contain HTML to render.
|
||||
* @returns {ReactElement[]|string}
|
||||
*/
|
||||
export function renderHTML(html?: string) {
|
||||
if (typeof html === 'string') {
|
||||
// At the time of this writing, the specified HTML contains a couple
|
||||
// of spaces one after the other. They do not cause a visible
|
||||
// problem on Web, because the specified HTML is rendered as, well,
|
||||
// HTML. However, we're not rendering HTML here.
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
html = html.replace(/\s{2,}/gi, ' ');
|
||||
|
||||
// Render text in <b>text</b> in bold.
|
||||
const opening = /<\s*b\s*>/gi;
|
||||
const closing = /<\s*\/\s*b\s*>/gi;
|
||||
let o;
|
||||
let c;
|
||||
let prevClosingLastIndex = 0;
|
||||
const r = [];
|
||||
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while (o = opening.exec(html)) {
|
||||
closing.lastIndex = opening.lastIndex;
|
||||
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
if (c = closing.exec(html)) {
|
||||
r.push(html.substring(prevClosingLastIndex, o.index));
|
||||
r.push(
|
||||
<Text style = { (styles.boldDialogText as TextStyle) }>
|
||||
{ html.substring(opening.lastIndex, c.index) }
|
||||
</Text>);
|
||||
opening.lastIndex
|
||||
= prevClosingLastIndex
|
||||
= closing.lastIndex;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (prevClosingLastIndex < html.length) {
|
||||
r.push(html.substring(prevClosingLastIndex));
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
168
react/features/base/dialog/components/native/AbstractDialog.ts
Normal file
168
react/features/base/dialog/components/native/AbstractDialog.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { Component } from 'react';
|
||||
|
||||
import { IStore } from '../../../../app/types';
|
||||
import { hideDialog } from '../../actions';
|
||||
import { DialogProps } from '../../constants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AbstractDialog}.
|
||||
*/
|
||||
export interface IProps extends DialogProps {
|
||||
|
||||
/**
|
||||
* Used to show/hide the dialog on cancel.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link AbstractDialog}.
|
||||
*/
|
||||
export interface IState {
|
||||
submitting?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstract implementation of a dialog on Web/React and mobile/react-native.
|
||||
*/
|
||||
export default class AbstractDialog<P extends IProps, S extends IState = IState>
|
||||
extends Component<P, S> {
|
||||
|
||||
_mounted: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code AbstractDialog} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onBack = this._onBack.bind(this);
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._onSubmitFulfilled = this._onSubmitFulfilled.bind(this);
|
||||
this._onSubmitRejected = this._onSubmitRejected.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount()}. Invoked
|
||||
* immediately before mounting occurs.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentDidMount() {
|
||||
this._mounted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentWillUnmount()}. Invoked
|
||||
* immediately before this component is unmounted and destroyed.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentWillUnmount() {
|
||||
this._mounted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a redux action to hide this dialog.
|
||||
*
|
||||
* @returns {*} The return value of {@link hideDialog}.
|
||||
*/
|
||||
_hide() {
|
||||
return this.props.dispatch(hideDialog());
|
||||
}
|
||||
|
||||
_onBack() {
|
||||
const { backDisabled = false, onBack } = this.props;
|
||||
|
||||
if (!backDisabled && (!onBack || onBack())) {
|
||||
this._hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a redux action to hide this dialog when it's canceled.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCancel() {
|
||||
const { cancelDisabled = false, onCancel } = this.props;
|
||||
|
||||
if (!cancelDisabled && (!onCancel || onCancel())) {
|
||||
this._hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits this {@code Dialog}. If the React {@code Component} prop
|
||||
* {@code onSubmit} is defined, the function that is the value of the prop
|
||||
* is invoked. If the function returns a {@code thenable}, then the
|
||||
* resolution of the {@code thenable} is awaited. If the submission
|
||||
* completes successfully, a redux action will be dispatched to hide this
|
||||
* dialog.
|
||||
*
|
||||
* @protected
|
||||
* @param {string} [value] - The submitted value if any.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSubmit(value?: string) {
|
||||
const { okDisabled = false, onSubmit } = this.props;
|
||||
|
||||
if (!okDisabled) {
|
||||
this.setState({ submitting: true });
|
||||
|
||||
// Invoke the React Component prop onSubmit if any.
|
||||
const r = !onSubmit || onSubmit(value);
|
||||
|
||||
// If the invocation returns a thenable, await its resolution;
|
||||
// otherwise, treat the return value as a boolean indicating whether
|
||||
// the submission has completed successfully.
|
||||
let then;
|
||||
|
||||
if (r) {
|
||||
switch (typeof r) {
|
||||
case 'function':
|
||||
case 'object':
|
||||
then = r.then;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (typeof then === 'function' && then.length === 2) {
|
||||
then.call(r, this._onSubmitFulfilled, this._onSubmitRejected);
|
||||
} else if (r) {
|
||||
this._onSubmitFulfilled();
|
||||
} else {
|
||||
this._onSubmitRejected();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies this {@code AbstractDialog} that it has been submitted
|
||||
* successfully. Dispatches a redux action to hide this dialog after it has
|
||||
* been submitted.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSubmitFulfilled() {
|
||||
this._mounted && this.setState({ submitting: false });
|
||||
|
||||
this._hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies this {@code AbstractDialog} that its submission has failed.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSubmitRejected() {
|
||||
this._mounted && this.setState({ submitting: false });
|
||||
}
|
||||
}
|
||||
56
react/features/base/dialog/components/native/AlertDialog.tsx
Normal file
56
react/features/base/dialog/components/native/AlertDialog.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import Dialog from 'react-native-dialog';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../i18n/functions';
|
||||
import { _abstractMapStateToProps } from '../../functions';
|
||||
import { renderHTML } from '../functions.native';
|
||||
|
||||
import AbstractDialog, { IProps as AbstractProps } from './AbstractDialog';
|
||||
|
||||
interface IProps extends AbstractProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* Untranslated i18n key of the content to be displayed.
|
||||
*
|
||||
* NOTE: This dialog also adds support to Object type keys that will be
|
||||
* translated using the provided params. See i18n function
|
||||
* {@code translate(string, Object)} for more details.
|
||||
*/
|
||||
contentKey: string | { key: string; params: Object; };
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements an alert dialog, to simply show an error or a message,
|
||||
* then disappear on dismiss.
|
||||
*/
|
||||
class AlertDialog extends AbstractDialog<IProps> {
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { contentKey, t } = this.props;
|
||||
const content
|
||||
= typeof contentKey === 'string'
|
||||
? t(contentKey)
|
||||
: renderHTML(t(contentKey.key, contentKey.params));
|
||||
|
||||
return (
|
||||
<Dialog.Container
|
||||
coverScreen = { false }
|
||||
visible = { true }>
|
||||
<Dialog.Description>
|
||||
{ content }
|
||||
</Dialog.Description>
|
||||
<Dialog.Button
|
||||
label = { t('dialog.Ok') }
|
||||
onPress = { this._onSubmit } />
|
||||
</Dialog.Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(_abstractMapStateToProps)(AlertDialog));
|
||||
150
react/features/base/dialog/components/native/BottomSheet.tsx
Normal file
150
react/features/base/dialog/components/native/BottomSheet.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React, { PureComponent, ReactNode } from 'react';
|
||||
import { SafeAreaView, ScrollView, View, ViewStyle } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IStore } from '../../../../app/types';
|
||||
import SlidingView from '../../../react/components/native/SlidingView';
|
||||
import { hideSheet } from '../../actions';
|
||||
|
||||
import { bottomSheetStyles as styles } from './styles';
|
||||
|
||||
/**
|
||||
* The type of {@code BottomSheet}'s React {@code Component} prop types.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Whether to add padding to scroll view.
|
||||
*/
|
||||
addScrollViewPadding?: boolean;
|
||||
|
||||
/**
|
||||
* The children to be displayed within this component.
|
||||
*/
|
||||
children: ReactNode;
|
||||
|
||||
/**
|
||||
* Redux Dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Handler for the cancel event, which happens when the user dismisses
|
||||
* the sheet.
|
||||
*/
|
||||
onCancel?: Function;
|
||||
|
||||
/**
|
||||
* Function to render a bottom sheet footer element, if necessary.
|
||||
*/
|
||||
renderFooter?: () => React.ReactNode;
|
||||
|
||||
/**
|
||||
* Function to render a bottom sheet header element, if necessary.
|
||||
*/
|
||||
renderHeader?: Function;
|
||||
|
||||
/**
|
||||
* Whether to show sliding view or not.
|
||||
*/
|
||||
showSlidingView?: boolean;
|
||||
|
||||
/**
|
||||
* The component's external style.
|
||||
*/
|
||||
style?: Object;
|
||||
};
|
||||
|
||||
/**
|
||||
* A component emulating Android's BottomSheet.
|
||||
*/
|
||||
class BottomSheet extends PureComponent<Props> {
|
||||
/**
|
||||
* Default values for {@code BottomSheet} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
addScrollViewPadding: true,
|
||||
showSlidingView: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new instance.
|
||||
*
|
||||
* @param {Props} props - The React {@code Component} props to initialize
|
||||
* the new instance with.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the cancel event, when the user dismissed the sheet. By default we close it.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCancel() {
|
||||
if (this.props.onCancel) {
|
||||
this.props.onCancel();
|
||||
} else {
|
||||
this.props.dispatch(hideSheet());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
addScrollViewPadding,
|
||||
renderHeader,
|
||||
renderFooter,
|
||||
showSlidingView,
|
||||
style
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<SlidingView
|
||||
onHide = { this._onCancel }
|
||||
position = 'bottom'
|
||||
show = { Boolean(showSlidingView) }>
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { styles.sheetContainer as ViewStyle }>
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { styles.sheetAreaCover } />
|
||||
{ renderHeader?.() }
|
||||
<SafeAreaView
|
||||
style = { [
|
||||
styles.sheetItemContainer,
|
||||
renderHeader
|
||||
? styles.sheetHeader
|
||||
: styles.sheet,
|
||||
renderFooter && styles.sheetFooter,
|
||||
style
|
||||
] }>
|
||||
<ScrollView
|
||||
bounces = { false }
|
||||
showsVerticalScrollIndicator = { false }
|
||||
style = { [
|
||||
renderFooter && styles.sheet,
|
||||
addScrollViewPadding && styles.scrollView
|
||||
] } >
|
||||
{ this.props.children }
|
||||
</ScrollView>
|
||||
{ renderFooter?.() }
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</SlidingView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect()(BottomSheet);
|
||||
@@ -0,0 +1,22 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
|
||||
|
||||
const BottomSheetContainer: () => JSX.Element | null = (): JSX.Element | null => {
|
||||
const { sheet, sheetProps } = useSelector((state: IReduxState) => state['features/base/dialog']);
|
||||
const { reducedUI } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
|
||||
|
||||
if (!sheet || reducedUI) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{ React.createElement(sheet, sheetProps) }
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default BottomSheetContainer;
|
||||
172
react/features/base/dialog/components/native/ConfirmDialog.tsx
Normal file
172
react/features/base/dialog/components/native/ConfirmDialog.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import Dialog from 'react-native-dialog';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../i18n/functions';
|
||||
import { renderHTML } from '../functions.native';
|
||||
|
||||
import AbstractDialog, { IProps as AbstractProps } from './AbstractDialog';
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link ConfirmDialog}.
|
||||
*/
|
||||
interface IProps extends AbstractProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* The i18n key of the text label for the back button.
|
||||
*/
|
||||
backLabel?: string;
|
||||
|
||||
/**
|
||||
* The i18n key of the text label for the cancel button.
|
||||
*/
|
||||
cancelLabel?: string;
|
||||
|
||||
/**
|
||||
* The React {@code Component} children.
|
||||
*/
|
||||
children?: React.ReactNode;
|
||||
|
||||
/**
|
||||
* The i18n key of the text label for the confirm button.
|
||||
*/
|
||||
confirmLabel?: string;
|
||||
|
||||
/**
|
||||
* Dialog description key for translations.
|
||||
*/
|
||||
descriptionKey?: string | { key: string; params: string; };
|
||||
|
||||
/**
|
||||
* Whether the back button is hidden.
|
||||
*/
|
||||
isBackHidden?: Boolean;
|
||||
|
||||
/**
|
||||
* Whether the cancel button is hidden.
|
||||
*/
|
||||
isCancelHidden?: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the nature of the confirm button is destructive.
|
||||
*/
|
||||
isConfirmDestructive?: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the confirm button is hidden.
|
||||
*/
|
||||
isConfirmHidden?: Boolean;
|
||||
|
||||
/**
|
||||
* Dialog title.
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* Renders buttons vertically.
|
||||
*/
|
||||
verticalButtons?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* React Component for getting confirmation to stop a file recording session in
|
||||
* progress.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class ConfirmDialog extends AbstractDialog<IProps> {
|
||||
/**
|
||||
* Default values for {@code ConfirmDialog} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
isConfirmDestructive: false,
|
||||
isConfirmHidden: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the dialog description.
|
||||
*
|
||||
* @returns {React$Component}
|
||||
*/
|
||||
_renderDescription() {
|
||||
const { descriptionKey, t } = this.props;
|
||||
const description
|
||||
= typeof descriptionKey === 'string'
|
||||
? t(descriptionKey)
|
||||
: renderHTML(
|
||||
t(descriptionKey?.key ?? '', descriptionKey?.params)
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog.Description>
|
||||
{ description }
|
||||
</Dialog.Description>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
backLabel,
|
||||
cancelLabel,
|
||||
children,
|
||||
confirmLabel,
|
||||
isBackHidden = true,
|
||||
isCancelHidden,
|
||||
isConfirmDestructive,
|
||||
isConfirmHidden,
|
||||
t,
|
||||
title,
|
||||
verticalButtons
|
||||
} = this.props;
|
||||
|
||||
const dialogButtonStyle
|
||||
= isConfirmDestructive
|
||||
? styles.destructiveDialogButton : styles.dialogButton;
|
||||
|
||||
return (
|
||||
<Dialog.Container
|
||||
coverScreen = { false }
|
||||
verticalButtons = { verticalButtons }
|
||||
visible = { true }>
|
||||
{
|
||||
title && <Dialog.Title>
|
||||
{ t(title) }
|
||||
</Dialog.Title>
|
||||
}
|
||||
{ this._renderDescription() }
|
||||
{ children }
|
||||
{
|
||||
!isBackHidden && <Dialog.Button
|
||||
label = { t(backLabel || 'dialog.confirmBack') }
|
||||
onPress = { this._onBack }
|
||||
style = { styles.dialogButton } />
|
||||
}
|
||||
{
|
||||
!isCancelHidden && <Dialog.Button
|
||||
label = { t(cancelLabel || 'dialog.confirmNo') }
|
||||
onPress = { this._onCancel }
|
||||
style = { styles.dialogButton } />
|
||||
}
|
||||
{
|
||||
!isConfirmHidden && <Dialog.Button
|
||||
label = { t(confirmLabel || 'dialog.confirmYes') }
|
||||
onPress = { this._onSubmit }
|
||||
style = { dialogButtonStyle } />
|
||||
}
|
||||
</Dialog.Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(ConfirmDialog));
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import ReactionEmoji from '../../../../reactions/components/native/ReactionEmoji';
|
||||
import { getReactionsQueue } from '../../../../reactions/functions.native';
|
||||
import AbstractDialogContainer, {
|
||||
abstractMapStateToProps
|
||||
} from '../AbstractDialogContainer';
|
||||
|
||||
/**
|
||||
* Implements a DialogContainer responsible for showing all dialogs. We will
|
||||
* need a separate container so we can handle multiple dialogs by showing them
|
||||
* simultaneously or queueing them.
|
||||
*
|
||||
* @augments AbstractDialogContainer
|
||||
*/
|
||||
class DialogContainer extends AbstractDialogContainer {
|
||||
|
||||
/**
|
||||
* Returns the reactions to be displayed.
|
||||
*
|
||||
* @returns {Array<React$Element>}
|
||||
*/
|
||||
_renderReactions() {
|
||||
const { _reactionsQueue } = this.props;
|
||||
|
||||
return _reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
|
||||
index = { index }
|
||||
key = { uid }
|
||||
reaction = { reaction }
|
||||
uid = { uid } />));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<Fragment>
|
||||
{this._renderReactions()}
|
||||
{this._renderDialogContent()}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IReduxState) => {
|
||||
return {
|
||||
...abstractMapStateToProps(state),
|
||||
_reactionsQueue: getReactionsQueue(state)
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(DialogContainer);
|
||||
170
react/features/base/dialog/components/native/InputDialog.tsx
Normal file
170
react/features/base/dialog/components/native/InputDialog.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { TextStyle } from 'react-native';
|
||||
import Dialog from 'react-native-dialog';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../i18n/functions';
|
||||
import { _abstractMapStateToProps } from '../../functions';
|
||||
|
||||
import AbstractDialog, {
|
||||
IProps as AbstractProps,
|
||||
IState as AbstractState
|
||||
} from './AbstractDialog';
|
||||
import { inputDialog as styles } from './styles';
|
||||
|
||||
interface IProps extends AbstractProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* The dialog descriptionKey.
|
||||
*/
|
||||
descriptionKey?: string;
|
||||
|
||||
/**
|
||||
* Whether to display the cancel button.
|
||||
*/
|
||||
disableCancel?: boolean;
|
||||
|
||||
/**
|
||||
* An optional initial value to initiate the field with.
|
||||
*/
|
||||
initialValue?: string;
|
||||
|
||||
/**
|
||||
* A message key to be shown for the user (e.g. An error that is defined after submitting the form).
|
||||
*/
|
||||
messageKey?: string;
|
||||
|
||||
/**
|
||||
* Props for the text input.
|
||||
*/
|
||||
textInputProps?: Object;
|
||||
|
||||
/**
|
||||
* The untranslated i18n key for the dialog title.
|
||||
*/
|
||||
titleKey?: string;
|
||||
|
||||
/**
|
||||
* Validating of the input.
|
||||
*/
|
||||
validateInput?: Function;
|
||||
}
|
||||
|
||||
interface IState extends AbstractState {
|
||||
|
||||
/**
|
||||
* The current value of the field.
|
||||
*/
|
||||
fieldValue?: string;
|
||||
|
||||
/**
|
||||
* The result of the input validation.
|
||||
*/
|
||||
isValid: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a single field input dialog component.
|
||||
*/
|
||||
class InputDialog extends AbstractDialog<IProps, IState> {
|
||||
/**
|
||||
* Instantiates a new {@code InputDialog}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
fieldValue: props.initialValue,
|
||||
isValid: props.validateInput ? props.validateInput(props.initialValue) : true,
|
||||
submitting: false
|
||||
};
|
||||
|
||||
this._onChangeText = this._onChangeText.bind(this);
|
||||
this._onSubmitValue = this._onSubmitValue.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
descriptionKey,
|
||||
messageKey,
|
||||
t,
|
||||
titleKey
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Dialog.Container
|
||||
coverScreen = { false }
|
||||
visible = { true }>
|
||||
<Dialog.Title>
|
||||
{ t(titleKey ?? '') }
|
||||
</Dialog.Title>
|
||||
{
|
||||
descriptionKey && (
|
||||
<Dialog.Description>
|
||||
{ t(descriptionKey) }
|
||||
</Dialog.Description>
|
||||
)
|
||||
}
|
||||
<Dialog.Input
|
||||
autoFocus = { true }
|
||||
onChangeText = { this._onChangeText }
|
||||
value = { this.state.fieldValue }
|
||||
{ ...this.props.textInputProps } />
|
||||
{
|
||||
messageKey && (
|
||||
<Dialog.Description
|
||||
style = { styles.formMessage as TextStyle }>
|
||||
{ t(messageKey) }
|
||||
</Dialog.Description>
|
||||
)
|
||||
}
|
||||
{!this.props.disableCancel && <Dialog.Button
|
||||
label = { t('dialog.Cancel') }
|
||||
onPress = { this._onCancel } />}
|
||||
<Dialog.Button
|
||||
disabled = { !this.state.isValid }
|
||||
label = { t('dialog.Ok') }
|
||||
onPress = { this._onSubmitValue } />
|
||||
</Dialog.Container>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the text in the field changes.
|
||||
*
|
||||
* @param {string} fieldValue - The updated field value.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChangeText(fieldValue: string) {
|
||||
if (this.props.validateInput) {
|
||||
this.setState({
|
||||
isValid: this.props.validateInput(fieldValue),
|
||||
fieldValue
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
fieldValue
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the value of this dialog is submitted.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onSubmitValue() {
|
||||
return this._onSubmit(this.state.fieldValue);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(_abstractMapStateToProps)(InputDialog));
|
||||
@@ -0,0 +1,221 @@
|
||||
// @ts-expect-error
|
||||
import { randomInt } from '@jitsi/js-utils/random';
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { appNavigate, reloadNow } from '../../../../app/actions.native';
|
||||
import { IReduxState, IStore } from '../../../../app/types';
|
||||
import { translate } from '../../../i18n/functions';
|
||||
import { isFatalJitsiConnectionError } from '../../../lib-jitsi-meet/functions.native';
|
||||
import { hideDialog } from '../../actions';
|
||||
import logger from '../../logger';
|
||||
|
||||
import ConfirmDialog from './ConfirmDialog';
|
||||
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link PageReloadDialog}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
conferenceError?: Error;
|
||||
configError?: Error;
|
||||
connectionError?: Error;
|
||||
dispatch: IStore['dispatch'];
|
||||
isNetworkFailure: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of
|
||||
* {@link PageReloadDialog}.
|
||||
*/
|
||||
interface IPageReloadDialogState {
|
||||
timeLeft: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React Component that is shown before the
|
||||
* conference is reloaded.
|
||||
* Shows a warning message and counts down towards the re-load.
|
||||
*/
|
||||
class PageReloadDialog extends Component<IProps, IPageReloadDialogState> {
|
||||
_interval?: number;
|
||||
_timeoutSeconds: number;
|
||||
|
||||
/**
|
||||
* Initializes a new PageReloadOverlay instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
* @public
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._timeoutSeconds = 10 + randomInt(0, 20);
|
||||
|
||||
this.state = {
|
||||
timeLeft: this._timeoutSeconds
|
||||
};
|
||||
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onReloadNow = this._onReloadNow.bind(this);
|
||||
this._onReconnecting = this._onReconnecting.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* React Component method that executes once component is mounted.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentDidMount() {
|
||||
const { timeLeft } = this.state;
|
||||
|
||||
logger.info(
|
||||
`The conference will be reloaded after ${timeLeft} seconds.`
|
||||
);
|
||||
|
||||
this._interval = setInterval(() =>
|
||||
this._onReconnecting(), 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the timer interval.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentWillUnmount() {
|
||||
if (this._interval) {
|
||||
clearInterval(this._interval);
|
||||
this._interval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle clicking of the "Cancel" button. It will navigate back to the
|
||||
* welcome page.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onCancel() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
clearInterval(this._interval ?? 0);
|
||||
dispatch(appNavigate(undefined));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles automatic reconnection.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onReconnecting() {
|
||||
const { dispatch } = this.props;
|
||||
const { timeLeft } = this.state;
|
||||
|
||||
if (timeLeft === 0) {
|
||||
if (this._interval) {
|
||||
dispatch(hideDialog());
|
||||
this._onReloadNow();
|
||||
this._interval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
timeLeft: timeLeft - 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle clicking on the "Reload Now" button. It will navigate to the same
|
||||
* conference URL as before immediately, without waiting for the timer to
|
||||
* kick in.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onReloadNow() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
clearInterval(this._interval ?? 0);
|
||||
dispatch(reloadNow());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const { isNetworkFailure, t } = this.props;
|
||||
const { timeLeft } = this.state;
|
||||
|
||||
let message, title;
|
||||
|
||||
if (isNetworkFailure) {
|
||||
title = 'dialog.conferenceDisconnectTitle';
|
||||
message = 'dialog.conferenceDisconnectMsg';
|
||||
} else {
|
||||
title = 'dialog.conferenceReloadTitle';
|
||||
message = 'dialog.conferenceReloadMsg';
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
cancelLabel = 'dialog.Cancel'
|
||||
confirmLabel = 'dialog.rejoinNow'
|
||||
descriptionKey = { `${t(message, { seconds: timeLeft })}` }
|
||||
onCancel = { this._onCancel }
|
||||
onSubmit = { this._onReloadNow }
|
||||
title = { title } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated component's props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {IProps} ownProps - The own props of the component.
|
||||
* @protected
|
||||
* @returns {{
|
||||
* isNetworkFailure: boolean,
|
||||
* reason: string
|
||||
* }}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState, ownProps: IProps) {
|
||||
const { conferenceError, configError, connectionError } = ownProps;
|
||||
const fatalConnectionError
|
||||
= connectionError && isFatalJitsiConnectionError(connectionError);
|
||||
const isNetworkFailure = Boolean(configError || fatalConnectionError);
|
||||
|
||||
let reason;
|
||||
|
||||
if (conferenceError) {
|
||||
reason = `error.conference.${conferenceError.name}`;
|
||||
} else if (connectionError) {
|
||||
reason = `error.conference.${connectionError.name}`;
|
||||
} else if (configError) {
|
||||
reason = `error.config.${configError.name}`;
|
||||
} else {
|
||||
logger.error('No reload reason defined!');
|
||||
}
|
||||
|
||||
return {
|
||||
isNetworkFailure,
|
||||
reason
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(PageReloadDialog));
|
||||
265
react/features/base/dialog/components/native/styles.ts
Normal file
265
react/features/base/dialog/components/native/styles.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import ColorSchemeRegistry from '../../../color-scheme/ColorSchemeRegistry';
|
||||
import { schemeColor } from '../../../color-scheme/functions';
|
||||
import { BoxModel } from '../../../styles/components/styles/BoxModel';
|
||||
import BaseTheme from '../../../ui/components/BaseTheme.native';
|
||||
import { PREFERRED_DIALOG_SIZE } from '../../constants';
|
||||
|
||||
const BORDER_RADIUS = 5;
|
||||
|
||||
/**
|
||||
* NOTE: These Material guidelines based values are currently only used in
|
||||
* dialogs (and related) but later on it would be nice to export it into a base
|
||||
* Material feature.
|
||||
*/
|
||||
export const MD_FONT_SIZE = 16;
|
||||
export const MD_ITEM_HEIGHT = 48;
|
||||
export const MD_ITEM_MARGIN_PADDING = BaseTheme.spacing[3];
|
||||
|
||||
/**
|
||||
* Reusable (colored) style for text in any branded dialogs.
|
||||
*/
|
||||
const brandedDialogText = {
|
||||
color: schemeColor('text'),
|
||||
fontSize: MD_FONT_SIZE,
|
||||
textAlign: 'center'
|
||||
};
|
||||
|
||||
const brandedDialogLabelStyle = {
|
||||
color: BaseTheme.palette.text01,
|
||||
flexShrink: 1,
|
||||
fontSize: MD_FONT_SIZE,
|
||||
opacity: 0.90
|
||||
};
|
||||
|
||||
const brandedDialogItemContainerStyle = {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
height: MD_ITEM_HEIGHT
|
||||
};
|
||||
|
||||
const brandedDialogIconStyle = {
|
||||
color: BaseTheme.palette.icon01,
|
||||
fontSize: 24
|
||||
};
|
||||
|
||||
export const inputDialog = {
|
||||
formMessage: {
|
||||
alignSelf: 'flex-start',
|
||||
fontStyle: 'italic',
|
||||
fontWeight: 'bold',
|
||||
marginTop: BaseTheme.spacing[3]
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The React {@code Component} styles of {@code BottomSheet}. These have
|
||||
* been implemented as per the Material Design guidelines:
|
||||
* {@link https://material.io/guidelines/components/bottom-sheets.html}.
|
||||
*/
|
||||
export const bottomSheetStyles = {
|
||||
sheetAreaCover: {
|
||||
backgroundColor: 'transparent',
|
||||
flex: 1
|
||||
},
|
||||
|
||||
scrollView: {
|
||||
paddingHorizontal: 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the container of the sheet.
|
||||
*/
|
||||
sheetContainer: {
|
||||
alignItems: 'stretch',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
maxWidth: 500,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
sheetItemContainer: {
|
||||
flex: -1,
|
||||
maxHeight: '75%'
|
||||
},
|
||||
|
||||
buttons: {
|
||||
/**
|
||||
* Style for the {@code Icon} element in a generic item of the menu.
|
||||
*/
|
||||
iconStyle: {
|
||||
...brandedDialogIconStyle
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the label in a generic item rendered in the menu.
|
||||
*/
|
||||
labelStyle: {
|
||||
...brandedDialogLabelStyle,
|
||||
marginLeft: 16
|
||||
},
|
||||
|
||||
/**
|
||||
* Container style for a generic item rendered in the menu.
|
||||
*/
|
||||
style: {
|
||||
...brandedDialogItemContainerStyle,
|
||||
paddingHorizontal: MD_ITEM_MARGIN_PADDING
|
||||
},
|
||||
|
||||
/**
|
||||
* Additional style that is not directly used as a style object.
|
||||
*/
|
||||
underlayColor: BaseTheme.palette.ui04
|
||||
},
|
||||
|
||||
/**
|
||||
* Bottom sheet's base style.
|
||||
*/
|
||||
sheet: {
|
||||
backgroundColor: BaseTheme.palette.ui02,
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16
|
||||
},
|
||||
|
||||
/**
|
||||
* Bottom sheet's base style with header.
|
||||
*/
|
||||
sheetHeader: {
|
||||
backgroundColor: BaseTheme.palette.ui02
|
||||
},
|
||||
|
||||
/**
|
||||
* Bottom sheet's background color with footer.
|
||||
*/
|
||||
sheetFooter: {
|
||||
backgroundColor: BaseTheme.palette.ui01
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
dialogButton: {
|
||||
...BaseTheme.typography.bodyLongBold
|
||||
},
|
||||
|
||||
destructiveDialogButton: {
|
||||
...BaseTheme.typography.bodyLongBold,
|
||||
color: BaseTheme.palette.actionDanger
|
||||
}
|
||||
};
|
||||
|
||||
export const brandedDialog = {
|
||||
|
||||
/**
|
||||
* The style of bold {@code Text} rendered by the {@code Dialog}s of the
|
||||
* feature authentication.
|
||||
*/
|
||||
boldDialogText: {
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
|
||||
buttonFarRight: {
|
||||
borderBottomRightRadius: BORDER_RADIUS
|
||||
},
|
||||
|
||||
buttonWrapper: {
|
||||
alignItems: 'stretch',
|
||||
borderRadius: BORDER_RADIUS,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
|
||||
mainWrapper: {
|
||||
alignSelf: 'stretch',
|
||||
padding: BoxModel.padding * 2,
|
||||
|
||||
// The added bottom padding is to compensate the empty space around the
|
||||
// close icon.
|
||||
paddingBottom: BoxModel.padding * 3
|
||||
},
|
||||
|
||||
overlayTouchable: {
|
||||
...StyleSheet.absoluteFillObject
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Color schemed styles for all the component based on the abstract dialog.
|
||||
*/
|
||||
ColorSchemeRegistry.register('Dialog', {
|
||||
button: {
|
||||
backgroundColor: '#44A5FF',
|
||||
flex: 1,
|
||||
padding: BoxModel.padding * 1.5
|
||||
},
|
||||
|
||||
/**
|
||||
* Separator line for the buttons in a dialog.
|
||||
*/
|
||||
buttonSeparator: {
|
||||
borderRightColor: schemeColor('border'),
|
||||
borderRightWidth: 1
|
||||
},
|
||||
|
||||
buttonLabel: {
|
||||
color: schemeColor('buttonLabel'),
|
||||
fontSize: MD_FONT_SIZE,
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* Style of the close icon on a dialog.
|
||||
*/
|
||||
closeStyle: {
|
||||
color: schemeColor('icon'),
|
||||
fontSize: MD_FONT_SIZE
|
||||
},
|
||||
|
||||
/**
|
||||
* Base style of the dialogs.
|
||||
*/
|
||||
dialog: {
|
||||
alignItems: 'stretch',
|
||||
backgroundColor: schemeColor('background'),
|
||||
borderColor: schemeColor('border'),
|
||||
borderRadius: BORDER_RADIUS,
|
||||
borderWidth: 1,
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
maxWidth: PREFERRED_DIALOG_SIZE
|
||||
},
|
||||
|
||||
/**
|
||||
* Field on an input dialog.
|
||||
*/
|
||||
field: {
|
||||
...brandedDialogText,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: schemeColor('border'),
|
||||
margin: BoxModel.margin,
|
||||
textAlign: 'left'
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the field label on an input dialog.
|
||||
*/
|
||||
fieldLabel: {
|
||||
...brandedDialogText,
|
||||
margin: BoxModel.margin,
|
||||
textAlign: 'left'
|
||||
},
|
||||
|
||||
text: {
|
||||
...brandedDialogText,
|
||||
color: BaseTheme.palette.text01
|
||||
},
|
||||
|
||||
topBorderContainer: {
|
||||
borderTopColor: BaseTheme.palette.ui07,
|
||||
borderTopWidth: 1
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Component } from 'react';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AbstractDialogTab}.
|
||||
*/
|
||||
export interface IProps {
|
||||
|
||||
/**
|
||||
* Callback to invoke on change.
|
||||
*/
|
||||
onTabStateChange: Function;
|
||||
|
||||
/**
|
||||
* The id of the tab.
|
||||
*/
|
||||
tabId: number;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Abstract React {@code Component} for tabs of the DialogWithTabs component.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class AbstractDialogTab<P extends IProps, S> extends Component<P, S> {
|
||||
/**
|
||||
* Initializes a new {@code AbstractDialogTab} instance.
|
||||
*
|
||||
* @param {P} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onChange = this._onChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the onTabStateChange function to pass the changed state of the
|
||||
* controlled tab component to the controlling DialogWithTabs component.
|
||||
*
|
||||
* @param {Object} change - Object that contains the changed property and
|
||||
* value.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChange(change: Object) {
|
||||
const { onTabStateChange, tabId } = this.props;
|
||||
|
||||
onTabStateChange(tabId, {
|
||||
...this.props,
|
||||
...change
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default AbstractDialogTab;
|
||||
83
react/features/base/dialog/constants.ts
Normal file
83
react/features/base/dialog/constants.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export type DialogProps = {
|
||||
|
||||
/**
|
||||
* Whether back button is disabled. Enabled by default.
|
||||
*/
|
||||
backDisabled?: boolean;
|
||||
|
||||
/**
|
||||
* Optional i18n key to change the back button title.
|
||||
*/
|
||||
backKey?: string;
|
||||
|
||||
/**
|
||||
* Whether cancel button is disabled. Enabled by default.
|
||||
*/
|
||||
cancelDisabled?: boolean;
|
||||
|
||||
/**
|
||||
* Optional i18n key to change the cancel button title.
|
||||
*/
|
||||
cancelKey?: string;
|
||||
|
||||
/**
|
||||
* The React {@code Component} children which represents the dialog's body.
|
||||
*/
|
||||
children?: ReactNode;
|
||||
|
||||
/**
|
||||
* Is ok button enabled/disabled. Enabled by default.
|
||||
*/
|
||||
okDisabled?: boolean;
|
||||
|
||||
/**
|
||||
* Optional i18n key to change the ok button title.
|
||||
*/
|
||||
okKey?: string;
|
||||
|
||||
/**
|
||||
* The handler for onBack event.
|
||||
*/
|
||||
onBack?: Function;
|
||||
|
||||
/**
|
||||
* The handler for onCancel event.
|
||||
*/
|
||||
onCancel?: Function;
|
||||
|
||||
/**
|
||||
* The handler for the event when submitting the dialog.
|
||||
*/
|
||||
onSubmit?: Function;
|
||||
|
||||
/**
|
||||
* Additional style to be applied on the dialog.
|
||||
*
|
||||
* NOTE: Not all dialog types support this!
|
||||
*/
|
||||
style?: Object;
|
||||
|
||||
/**
|
||||
* Key to use for showing a title.
|
||||
*/
|
||||
titleKey?: string;
|
||||
|
||||
/**
|
||||
* The string to use as a title instead of {@code titleKey}. If a truthy
|
||||
* value is specified, it takes precedence over {@code titleKey} i.e.
|
||||
* The latter is unused.
|
||||
*/
|
||||
titleString?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A preferred (or optimal) dialog size. This constant is reused in many
|
||||
* components, where dialog size optimization is suggested.
|
||||
*
|
||||
* NOTE: Even though we support valious devices, including tablets, we don't
|
||||
* want the dialogs to be oversized even on larger devices. This number seems
|
||||
* to be a good compromise, but also easy to update.
|
||||
*/
|
||||
export const PREFERRED_DIALOG_SIZE = 300;
|
||||
45
react/features/base/dialog/functions.ts
Normal file
45
react/features/base/dialog/functions.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { IStateful } from '../app/types';
|
||||
import ColorSchemeRegistry from '../color-scheme/ColorSchemeRegistry';
|
||||
import { toState } from '../redux/functions';
|
||||
|
||||
/**
|
||||
* Checks if any {@code Dialog} is currently open.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store, the redux
|
||||
* {@code getState} function, or the redux state itself.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAnyDialogOpen(stateful: IStateful) {
|
||||
return Boolean(toState(stateful)['features/base/dialog'].component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a {@code Dialog} with a specific {@code component} is currently
|
||||
* open.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store, the redux
|
||||
* {@code getState} function, or the redux state itself.
|
||||
* @param {React.Component} component - The {@code component} of a
|
||||
* {@code Dialog} to be checked.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isDialogOpen(stateful: IStateful, component: ComponentType<any>) {
|
||||
return toState(stateful)['features/base/dialog'].component === component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of any Dialog based component.
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {{
|
||||
* _dialogStyles: StyleType
|
||||
* }}
|
||||
*/
|
||||
export function _abstractMapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_dialogStyles: ColorSchemeRegistry.get(state, 'Dialog')
|
||||
};
|
||||
}
|
||||
3
react/features/base/dialog/logger.ts
Normal file
3
react/features/base/dialog/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/dialog');
|
||||
63
react/features/base/dialog/reducer.ts
Normal file
63
react/features/base/dialog/reducer.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
import ReducerRegistry from '../redux/ReducerRegistry';
|
||||
import { assign } from '../redux/functions';
|
||||
|
||||
import {
|
||||
HIDE_DIALOG,
|
||||
HIDE_SHEET,
|
||||
OPEN_DIALOG,
|
||||
OPEN_SHEET
|
||||
} from './actionTypes';
|
||||
|
||||
export interface IDialogState {
|
||||
component?: ComponentType;
|
||||
componentProps?: Object;
|
||||
sheet?: ComponentType;
|
||||
sheetProps?: Object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces redux actions which show or hide dialogs.
|
||||
*
|
||||
* @param {IDialogState} state - The current redux state.
|
||||
* @param {Action} action - The redux action to reduce.
|
||||
* @param {string} action.type - The type of the redux action to reduce..
|
||||
* @returns {State} The next redux state that is the result of reducing the
|
||||
* specified action.
|
||||
*/
|
||||
ReducerRegistry.register<IDialogState>('features/base/dialog', (state = {}, action): IDialogState => {
|
||||
switch (action.type) {
|
||||
case HIDE_DIALOG: {
|
||||
const { component } = action;
|
||||
|
||||
if (typeof component === 'undefined' || state.component === component) {
|
||||
return assign(state, {
|
||||
component: undefined,
|
||||
componentProps: undefined
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case OPEN_DIALOG:
|
||||
return assign(state, {
|
||||
component: action.component,
|
||||
componentProps: action.componentProps
|
||||
});
|
||||
|
||||
case HIDE_SHEET:
|
||||
return assign(state, {
|
||||
sheet: undefined,
|
||||
sheetProps: undefined
|
||||
});
|
||||
|
||||
case OPEN_SHEET:
|
||||
return assign(state, {
|
||||
sheet: action.component,
|
||||
sheetProps: action.componentProps
|
||||
});
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Checks whether the chrome extensions defined in the config file are installed or not.
|
||||
*
|
||||
* @param {Object} _config - Objects containing info about the configured extensions.
|
||||
*
|
||||
* @returns {Promise[]}
|
||||
*/
|
||||
export default function checkChromeExtensionsInstalled(_config: any = {}) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Checks whether the chrome extensions defined in the config file are installed or not.
|
||||
*
|
||||
* @param {Object} config - Objects containing info about the configured extensions.
|
||||
*
|
||||
* @returns {Promise[]}
|
||||
*/
|
||||
export default function checkChromeExtensionsInstalled(config: any = {}) {
|
||||
const isExtensionInstalled = (info: any) => new Promise(resolve => {
|
||||
const img = new Image();
|
||||
|
||||
img.src = `chrome-extension://${info.id}/${info.path}`;
|
||||
img.setAttribute('aria-hidden', 'true');
|
||||
img.onload = function() {
|
||||
resolve(true);
|
||||
};
|
||||
img.onerror = function() {
|
||||
resolve(false);
|
||||
};
|
||||
});
|
||||
const extensionInstalledFunction = (info: any) => isExtensionInstalled(info);
|
||||
|
||||
return Promise.all(
|
||||
(config.chromeExtensionsInfo || []).map((info: any) => extensionInstalledFunction(info))
|
||||
);
|
||||
}
|
||||
122
react/features/base/environment/environment.ts
Normal file
122
react/features/base/environment/environment.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import JitsiMeetJS from '../lib-jitsi-meet';
|
||||
import Platform from '../react/Platform';
|
||||
|
||||
import { isMobileBrowser } from './utils';
|
||||
|
||||
const { browser } = JitsiMeetJS.util;
|
||||
|
||||
const DEFAULT_OPTIMAL_BROWSERS = [
|
||||
'chrome',
|
||||
'chromium',
|
||||
'electron',
|
||||
'firefox',
|
||||
'safari',
|
||||
'webkit'
|
||||
];
|
||||
|
||||
const DEFAULT_UNSUPPORTED_BROWSERS: string[] = [];
|
||||
|
||||
const browserNameToCheck = {
|
||||
chrome: browser.isChrome.bind(browser),
|
||||
chromium: browser.isChromiumBased.bind(browser),
|
||||
electron: browser.isElectron.bind(browser),
|
||||
firefox: browser.isFirefox.bind(browser),
|
||||
safari: browser.isSafari.bind(browser),
|
||||
webkit: browser.isWebKitBased.bind(browser)
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether or not jitsi is optimized and targeted for the provided
|
||||
* browser name.
|
||||
*
|
||||
* @param {string} browserName - The name of the browser to check.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isBrowsersOptimal(browserName: string) {
|
||||
return (interfaceConfig.OPTIMAL_BROWSERS || DEFAULT_OPTIMAL_BROWSERS)
|
||||
.includes(browserName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the current OS is Mac.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isMacOS() {
|
||||
return Platform.OS === 'macos';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the current OS is Windows.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isWindows() {
|
||||
return Platform.OS === 'windows';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the current browser or the list of passed in browsers
|
||||
* is considered suboptimal. Suboptimal means it is a supported browser but has
|
||||
* not been explicitly listed as being optimal, possibly due to functionality
|
||||
* issues.
|
||||
*
|
||||
* @param {Array<string>} [browsers] - A list of browser names to check. Will
|
||||
* default to a whitelist.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isSuboptimalBrowser() {
|
||||
const optimalBrowsers
|
||||
= interfaceConfig.OPTIMAL_BROWSERS || DEFAULT_OPTIMAL_BROWSERS;
|
||||
|
||||
return !_isCurrentBrowserInList(optimalBrowsers) && isSupportedBrowser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the current browser should allow the app to display.
|
||||
* A supported browser is assumed to be able to support WebRtc.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isSupportedBrowser() {
|
||||
if (navigator.product === 'ReactNative') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Blacklists apply to desktop browsers only right now.
|
||||
if (!isMobileBrowser() && _isCurrentBrowserInList(
|
||||
interfaceConfig.UNSUPPORTED_BROWSERS || DEFAULT_UNSUPPORTED_BROWSERS
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isMobileBrowser() ? isSupportedMobileBrowser() : JitsiMeetJS.isWebRtcSupported();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the current environment is a supported
|
||||
* browser on a mobile device.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isSupportedMobileBrowser() {
|
||||
return (Platform.OS === 'android' && browser.isSupportedAndroidBrowser())
|
||||
|| (Platform.OS === 'ios' && browser.isSupportedIOSBrowser());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs various browser checks to know if the current browser is found within
|
||||
* the list.
|
||||
*
|
||||
* @param {Array<string>} list - Browser names to check. The names should be
|
||||
* keys in {@link browserNameToCheck}.
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function _isCurrentBrowserInList(list: string[]) {
|
||||
return Boolean(list.find(browserName => {
|
||||
const checkFunction = browserNameToCheck[browserName as keyof typeof browserNameToCheck];
|
||||
|
||||
return checkFunction ? checkFunction.call(browser) : false;
|
||||
}));
|
||||
}
|
||||
32
react/features/base/environment/utils.ts
Normal file
32
react/features/base/environment/utils.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import Platform from '../react/Platform';
|
||||
|
||||
/**
|
||||
* Returns whether or not the current environment is a mobile device.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isMobileBrowser() {
|
||||
return Platform.OS === 'android' || Platform.OS === 'ios';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether or not the current environment is an ios mobile device.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isIosMobileBrowser() {
|
||||
return Platform.OS === 'ios';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the current environment is an ipad device.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isIpadMobileBrowser() {
|
||||
|
||||
// @ts-ignore
|
||||
return isIosMobileBrowser() && Platform.isPad;
|
||||
}
|
||||
|
||||
10
react/features/base/flags/actionTypes.ts
Normal file
10
react/features/base/flags/actionTypes.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* The type of Redux action which updates the feature flags.
|
||||
*
|
||||
* {
|
||||
* type: UPDATE_FLAGS,
|
||||
* flags: Object
|
||||
* }
|
||||
*
|
||||
*/
|
||||
export const UPDATE_FLAGS = 'UPDATE_FLAGS';
|
||||
17
react/features/base/flags/actions.ts
Normal file
17
react/features/base/flags/actions.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { UPDATE_FLAGS } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Updates the current features flags with the given ones. They will be merged.
|
||||
*
|
||||
* @param {Object} flags - The new flags object.
|
||||
* @returns {{
|
||||
* type: UPDATE_FLAGS,
|
||||
* flags: Object
|
||||
* }}
|
||||
*/
|
||||
export function updateFlags(flags: Object) {
|
||||
return {
|
||||
type: UPDATE_FLAGS,
|
||||
flags
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user