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

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

View File

@@ -0,0 +1,160 @@
// @ts-ignore
// eslint-disable-next-line
import { openTokenAuthUrl } from '../authentication/actions';
// @ts-ignore
import { getTokenAuthUrl, isTokenAuthEnabled } from '../authentication/functions';
import { getJwtExpirationDate } from '../base/jwt/functions';
import { MEDIA_TYPE } from '../base/media/constants';
import { isLocalTrackMuted } from '../base/tracks/functions.any';
import { getLocationContextRoot, parseURIString } from '../base/util/uri';
import { addTrackStateToURL } from './functions.any';
import logger from './logger';
import { IStore } from './types';
/**
* Redirects to another page generated by replacing the path in the original URL
* with the given path.
*
* @param {(string)} pathname - The path to navigate to.
* @returns {Function}
*/
export function redirectWithStoredParams(pathname: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const { locationURL } = getState()['features/base/connection'];
const newLocationURL = new URL(locationURL?.href ?? '');
newLocationURL.pathname = pathname;
window.location.assign(newLocationURL.toString());
};
}
/**
* Assigns a specific pathname to window.location.pathname taking into account
* the context root of the Web app.
*
* @param {string} pathname - The pathname to assign to
* window.location.pathname. If the specified pathname is relative, the context
* root of the Web app will be prepended to the specified pathname before
* assigning it to window.location.pathname.
* @param {string} hashParam - Optional hash param to assign to
* window.location.hash.
* @returns {Function}
*/
export function redirectToStaticPage(pathname: string, hashParam?: string) {
return () => {
const windowLocation = window.location;
let newPathname = pathname;
if (!newPathname.startsWith('/')) {
// A pathname equal to ./ specifies the current directory. It will be
// fine but pointless to include it because contextRoot is the current
// directory.
newPathname.startsWith('./')
&& (newPathname = newPathname.substring(2));
newPathname = getLocationContextRoot(windowLocation) + newPathname;
}
if (hashParam) {
windowLocation.hash = hashParam;
}
windowLocation.pathname = newPathname;
};
}
/**
* Reloads the page by restoring the original URL.
*
* @returns {Function}
*/
export function reloadWithStoredParams() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const { locationURL } = state['features/base/connection'];
// Preserve the local tracks muted states.
// @ts-ignore
const newURL = addTrackStateToURL(locationURL, state);
const windowLocation = window.location;
const oldSearchString = windowLocation.search;
windowLocation.replace(newURL.toString());
if (newURL.search === oldSearchString) {
// NOTE: Assuming that only the hash or search part of the URL will
// be changed!
// location.replace will not trigger redirect/reload when
// only the hash params are changed. That's why we need to call
// reload in addition to replace.
windowLocation.reload();
}
};
}
/**
* Checks whether tokenAuthUrl is set, we have a jwt token that will expire soon
* and redirect to the auth url to obtain new token if this is the case.
*
* @param {Dispatch} dispatch - The Redux dispatch function.
* @param {Function} getState - The Redux state.
* @param {Function} failureCallback - The callback on failure to obtain auth url.
* @returns {boolean} Whether we will redirect or not.
*/
export function maybeRedirectToTokenAuthUrl(
dispatch: IStore['dispatch'], getState: IStore['getState'], failureCallback: Function) {
const state = getState();
const config = state['features/base/config'];
const { enabled: audioOnlyEnabled } = state['features/base/audio-only'];
const { startAudioOnly } = config;
const { locationURL = { href: '' } as URL } = state['features/base/connection'];
const audioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
const videoMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO);
if (!isTokenAuthEnabled(config)) {
return false;
}
// if tokenAuthUrl check jwt if is about to expire go through the url to get new token
const jwt = state['features/base/jwt'].jwt;
const expirationDate = getJwtExpirationDate(jwt);
// if there is jwt and its expiration time is less than 3 minutes away
// let's obtain new token
if (expirationDate && expirationDate.getTime() - Date.now() < 3 * 60 * 1000) {
const room = state['features/base/conference'].room;
const { tenant } = parseURIString(locationURL.href) || {};
getTokenAuthUrl(
config,
locationURL,
{
audioMuted,
audioOnlyEnabled: audioOnlyEnabled || startAudioOnly,
skipPrejoin: true,
videoMuted
},
room,
tenant
)
.then((tokenAuthServiceUrl: string | undefined) => {
if (!tokenAuthServiceUrl) {
logger.warn('Cannot handle login, token service URL is not set');
return Promise.reject();
}
return dispatch(openTokenAuthUrl(tokenAuthServiceUrl));
})
.catch(() => {
failureCallback();
});
return true;
}
return false;
}

View File

@@ -0,0 +1,222 @@
import { setRoom } from '../base/conference/actions';
import { getConferenceState } from '../base/conference/functions';
import {
configWillLoad,
loadConfigError,
setConfig,
storeConfig
} from '../base/config/actions';
import {
createFakeConfig,
restoreConfig
} from '../base/config/functions.native';
import { connect, disconnect, setLocationURL } from '../base/connection/actions.native';
import { JITSI_CONNECTION_URL_KEY } from '../base/connection/constants';
import { loadConfig } from '../base/lib-jitsi-meet/functions.native';
import { createDesiredLocalTracks } from '../base/tracks/actions.native';
import isInsecureRoomName from '../base/util/isInsecureRoomName';
import { parseURLParams } from '../base/util/parseURLParams';
import {
appendURLParam,
getBackendSafeRoomName,
parseURIString,
toURLString
} from '../base/util/uri';
import { isPrejoinPageEnabled } from '../mobile/navigation/functions';
import {
goBackToRoot,
navigateRoot
} from '../mobile/navigation/rootNavigationContainerRef';
import { screen } from '../mobile/navigation/routes';
import { clearNotifications } from '../notifications/actions';
import { isUnsafeRoomWarningEnabled } from '../prejoin/functions';
import { maybeRedirectToTokenAuthUrl } from './actions.any';
import { addTrackStateToURL, getDefaultURL } from './functions.native';
import logger from './logger';
import { IReloadNowOptions, IStore } from './types';
export * from './actions.any';
/**
* Triggers an in-app navigation to a specific route. Allows navigation to be
* abstracted between the mobile/React Native and Web/React applications.
*
* @param {string|undefined} uri - The URI to which to navigate. It may be a
* full URL with an HTTP(S) scheme, a full or partial URI with the app-specific
* scheme, or a mere room name.
* @param {Object} [options] - Options.
* @returns {Function}
*/
export function appNavigate(uri?: string, options: IReloadNowOptions = {}) {
logger.info(`appNavigate to ${uri}`);
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
let location = parseURIString(uri);
// If the specified location (URI) does not identify a host, use the app's
// default.
if (!location?.host) {
const defaultLocation = parseURIString(getDefaultURL(getState));
if (location) {
location.host = defaultLocation.host;
// FIXME Turn location's host, hostname, and port properties into
// setters in order to reduce the risks of inconsistent state.
location.hostname = defaultLocation.hostname;
location.pathname
= defaultLocation.pathname + location.pathname.substr(1);
location.port = defaultLocation.port;
location.protocol = defaultLocation.protocol;
} else {
location = defaultLocation;
}
}
location.protocol || (location.protocol = 'https:');
const { contextRoot, host, hostname, pathname, room } = location;
const locationURL = new URL(location.toString());
const { conference } = getConferenceState(getState());
if (room) {
if (conference) {
// We need to check if the location is the same with the previous one.
const currentLocationURL = conference?.getConnection()[JITSI_CONNECTION_URL_KEY];
const { hostname: currentHostName, pathname: currentPathName } = currentLocationURL;
if (currentHostName === hostname && currentPathName === pathname) {
logger.warn(`Joining same conference using URL: ${currentLocationURL}`);
return;
}
} else {
navigateRoot(screen.connecting);
}
}
dispatch(disconnect());
dispatch(configWillLoad(locationURL, room));
let protocol = location.protocol.toLowerCase();
// The React Native app supports an app-specific scheme which is sure to not
// be supported by fetch.
protocol !== 'http:' && protocol !== 'https:' && (protocol = 'https:');
const baseURL = `${protocol}//${host}${contextRoot || '/'}`;
let url = `${baseURL}config.js`;
// XXX In order to support multiple shards, tell the room to the deployment.
room && (url = appendURLParam(url, 'room', getBackendSafeRoomName(room) ?? ''));
const { release } = parseURLParams(location, true, 'search');
release && (url = appendURLParam(url, 'release', release));
let config;
// Avoid (re)loading the config when there is no room.
if (!room) {
config = restoreConfig(baseURL);
}
if (!config) {
try {
config = await loadConfig(url);
dispatch(storeConfig(baseURL, config));
} catch (error: any) {
config = restoreConfig(baseURL);
if (!config) {
if (room) {
dispatch(loadConfigError(error, locationURL));
return;
}
// If there is no room (we are on the welcome page), don't fail, just create a fake one.
logger.warn('Failed to load config but there is no room, applying a fake one');
config = createFakeConfig(baseURL);
}
}
}
if (getState()['features/base/config'].locationURL !== locationURL) {
dispatch(loadConfigError(new Error('Config no longer needed!'), locationURL));
return;
}
dispatch(setLocationURL(locationURL));
dispatch(setConfig(config));
dispatch(setRoom(room));
if (!room) {
goBackToRoot(getState(), dispatch);
return;
}
dispatch(createDesiredLocalTracks());
dispatch(clearNotifications());
if (!options.hidePrejoin && isPrejoinPageEnabled(getState())) {
if (isUnsafeRoomWarningEnabled(getState()) && isInsecureRoomName(room)) {
navigateRoot(screen.unsafeRoomWarning);
} else {
navigateRoot(screen.preJoin);
}
} else {
dispatch(connect());
navigateRoot(screen.conference.root);
}
};
}
/**
* Check if the welcome page is enabled and redirects to it.
* If requested show a thank you dialog before that.
* If we have a close page enabled, redirect to it without
* showing any other dialog.
*
* @param {Object} _options - Ignored.
* @returns {Function}
*/
export function maybeRedirectToWelcomePage(_options?: any): any {
// Dummy.
}
/**
* Reloads the page.
*
* @protected
* @returns {Function}
*/
export function reloadNow() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const { locationURL } = state['features/base/connection'];
// Preserve the local tracks muted state after the reload.
// @ts-ignore
const newURL = addTrackStateToURL(locationURL, state);
const reloadAction = () => {
logger.info(`Reloading the conference using URL: ${locationURL}`);
dispatch(appNavigate(toURLString(newURL), {
hidePrejoin: true
}));
};
if (maybeRedirectToTokenAuthUrl(dispatch, getState, reloadAction)) {
return;
}
reloadAction();
};
}

View File

@@ -0,0 +1,186 @@
// @ts-expect-error
import { API_ID } from '../../../modules/API';
import { setRoom } from '../base/conference/actions';
import {
configWillLoad,
setConfig
} from '../base/config/actions';
import { setLocationURL } from '../base/connection/actions.web';
import { loadConfig } from '../base/lib-jitsi-meet/functions.web';
import { isEmbedded } from '../base/util/embedUtils';
import { parseURIString } from '../base/util/uri';
import { isVpaasMeeting } from '../jaas/functions';
import { clearNotifications, showNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import { isWelcomePageEnabled } from '../welcome/functions';
import {
maybeRedirectToTokenAuthUrl,
redirectToStaticPage,
redirectWithStoredParams,
reloadWithStoredParams
} from './actions.any';
import { getDefaultURL, getName } from './functions.web';
import logger from './logger';
import { IStore } from './types';
export * from './actions.any';
/**
* Triggers an in-app navigation to a specific route. Allows navigation to be
* abstracted between the mobile/React Native and Web/React applications.
*
* @param {string|undefined} uri - The URI to which to navigate. It may be a
* full URL with an HTTP(S) scheme, a full or partial URI with the app-specific
* scheme, or a mere room name.
* @returns {Function}
*/
export function appNavigate(uri?: string) {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
let location = parseURIString(uri);
// If the specified location (URI) does not identify a host, use the app's
// default.
if (!location?.host) {
const defaultLocation = parseURIString(getDefaultURL(getState));
if (location) {
location.host = defaultLocation.host;
// FIXME Turn location's host, hostname, and port properties into
// setters in order to reduce the risks of inconsistent state.
location.hostname = defaultLocation.hostname;
location.pathname
= defaultLocation.pathname + location.pathname.substr(1);
location.port = defaultLocation.port;
location.protocol = defaultLocation.protocol;
} else {
location = defaultLocation;
}
}
location.protocol || (location.protocol = 'https:');
const { room } = location;
const locationURL = new URL(location.toString());
// There are notifications now that gets displayed after we technically left
// the conference, but we're still on the conference screen.
dispatch(clearNotifications());
dispatch(configWillLoad(locationURL, room));
const config = await loadConfig();
dispatch(setLocationURL(locationURL));
dispatch(setConfig(config));
dispatch(setRoom(room));
};
}
/**
* Check if the welcome page is enabled and redirects to it.
* If requested show a thank you dialog before that.
* If we have a close page enabled, redirect to it without
* showing any other dialog.
*
* @param {Object} options - Used to decide which particular close page to show
* or if close page is disabled, whether we should show the thankyou dialog.
* @param {boolean} options.showThankYou - Whether we should
* show thank you dialog.
* @param {boolean} options.feedbackSubmitted - Whether feedback was submitted.
* @returns {Function}
*/
export function maybeRedirectToWelcomePage(options: { feedbackSubmitted?: boolean; showThankYou?: boolean; } = {}) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const {
enableClosePage
} = getState()['features/base/config'];
// if close page is enabled redirect to it, without further action
if (enableClosePage) {
if (isVpaasMeeting(getState())) {
const isOpenedInIframe = isEmbedded();
if (isOpenedInIframe) {
// @ts-ignore
window.location = 'about:blank';
} else {
dispatch(redirectToStaticPage('/'));
}
return;
}
const { jwt } = getState()['features/base/jwt'];
let hashParam;
// save whether current user is guest or not, and pass auth token,
// before navigating to close page
window.sessionStorage.setItem('guest', (!jwt).toString());
window.sessionStorage.setItem('jwt', jwt ?? '');
let path = 'close.html';
if (interfaceConfig.SHOW_PROMOTIONAL_CLOSE_PAGE) {
if (Number(API_ID) === API_ID) {
hashParam = `#jitsi_meet_external_api_id=${API_ID}`;
}
path = 'close3.html';
} else if (!options.feedbackSubmitted) {
path = 'close2.html';
}
dispatch(redirectToStaticPage(`static/${path}`, hashParam));
return;
}
// else: show thankYou dialog only if there is no feedback
if (options.showThankYou) {
dispatch(showNotification({
titleArguments: { appName: getName() },
titleKey: 'dialog.thankYou'
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
}
// if Welcome page is enabled redirect to welcome page after 3 sec, if
// there is a thank you message to be shown, 0.5s otherwise.
if (isWelcomePageEnabled(getState())) {
setTimeout(
() => {
dispatch(redirectWithStoredParams('/'));
},
options.showThankYou ? 3000 : 500);
}
};
}
/**
* Reloads the page.
*
* @protected
* @returns {Function}
*/
export function reloadNow() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const { locationURL } = state['features/base/connection'];
const reloadAction = () => {
logger.info(`Reloading the conference using URL: ${locationURL}`);
dispatch(reloadWithStoredParams());
};
if (maybeRedirectToTokenAuthUrl(dispatch, getState, reloadAction)) {
return;
}
reloadAction();
};
}

View File

@@ -0,0 +1,89 @@
import BaseApp from '../../base/app/components/BaseApp';
import { toURLString } from '../../base/util/uri';
import { appNavigate } from '../actions';
import { getDefaultURL } from '../functions';
/**
* The type of React {@code Component} props of {@link AbstractApp}.
*/
export interface IProps {
/**
* XXX Refer to the implementation of loadURLObject: in
* ios/sdk/src/JitsiMeetView.m for further information.
*/
timestamp: number;
/**
* The URL, if any, with which the app was launched.
*/
url: Object | string;
}
/**
* Base (abstract) class for main App component.
*
* @abstract
*/
export class AbstractApp<P extends IProps = IProps> extends BaseApp<P> {
/**
* Initializes the app.
*
* @inheritdoc
*/
override async componentDidMount() {
await super.componentDidMount();
// If a URL was explicitly specified to this React Component, then
// open it; otherwise, use a default.
this._openURL(toURLString(this.props.url) || this._getDefaultURL());
}
/**
* Implements React Component's componentDidUpdate.
*
* @inheritdoc
*/
override async componentDidUpdate(prevProps: IProps) {
const previousUrl = toURLString(prevProps.url);
const currentUrl = toURLString(this.props.url);
const previousTimestamp = prevProps.timestamp;
const currentTimestamp = this.props.timestamp;
await this._init.promise;
// Deal with URL changes.
if (previousUrl !== currentUrl
// XXX Refer to the implementation of loadURLObject: in
// ios/sdk/src/JitsiMeetView.m for further information.
|| previousTimestamp !== currentTimestamp) {
this._openURL(currentUrl || this._getDefaultURL());
}
}
/**
* Gets the default URL to be opened when this {@code App} mounts.
*
* @protected
* @returns {string} The default URL to be opened when this {@code App}
* mounts.
*/
_getDefaultURL() {
// @ts-ignore
return getDefaultURL(this.state.store);
}
/**
* Navigates this {@code AbstractApp} to (i.e. Opens) a specific URL.
*
* @param {Object|string} url - The URL to navigate this {@code AbstractApp}
* to (i.e. The URL to open).
* @protected
* @returns {void}
*/
_openURL(url: string | Object) {
this.state.store?.dispatch(appNavigate(toURLString(url)));
}
}

View File

@@ -0,0 +1,310 @@
import React, { ComponentType } from 'react';
import { NativeModules, Platform, StyleSheet, View } from 'react-native';
import DeviceInfo from 'react-native-device-info';
import { SafeAreaProvider } from 'react-native-safe-area-context';
// @ts-ignore
import { hideSplash } from 'react-native-splash-view';
import BottomSheetContainer from '../../base/dialog/components/native/BottomSheetContainer';
import DialogContainer from '../../base/dialog/components/native/DialogContainer';
import { updateFlags } from '../../base/flags/actions';
import { CALL_INTEGRATION_ENABLED } from '../../base/flags/constants';
import { clientResized, setSafeAreaInsets } from '../../base/responsive-ui/actions';
import DimensionsDetector from '../../base/responsive-ui/components/DimensionsDetector.native';
import { updateSettings } from '../../base/settings/actions';
import JitsiThemePaperProvider from '../../base/ui/components/JitsiThemeProvider.native';
import { isEmbedded } from '../../base/util/embedUtils.native';
import { _getRouteToRender } from '../getRouteToRender.native';
import logger from '../logger';
import { AbstractApp, IProps as AbstractAppProps } from './AbstractApp';
// Register middlewares and reducers.
import '../middlewares.native';
import '../reducers.native';
declare let __DEV__: any;
const { AppInfo } = NativeModules;
const DialogContainerWrapper = Platform.select({
default: View
});
/**
* The type of React {@code Component} props of {@link App}.
*/
interface IProps extends AbstractAppProps {
/**
* An object with the feature flags.
*/
flags: any;
/**
* An object with user information (display name, email, avatar URL).
*/
userInfo?: Object;
}
/**
* Root app {@code Component} on mobile/React Native.
*
* @augments AbstractApp
*/
export class App extends AbstractApp<IProps> {
/**
* Initializes a new {@code App} instance.
*
* @param {IProps} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props: IProps) {
super(props);
// In the Release configuration, React Native will (intentionally) throw
// an unhandled JavascriptException for an unhandled JavaScript error.
// This will effectively kill the app. In accord with the Web, do not
// kill the app.
this._maybeDisableExceptionsManager();
// Bind event handler so it is only bound once per instance.
this._onDimensionsChanged = this._onDimensionsChanged.bind(this);
this._onSafeAreaInsetsChanged = this._onSafeAreaInsetsChanged.bind(this);
}
/**
* Initializes the color scheme.
*
* @inheritdoc
*
* @returns {void}
*/
override async componentDidMount() {
await super.componentDidMount();
hideSplash();
const liteTxt = AppInfo.isLiteSDK ? ' (lite)' : '';
logger.info(`Loaded SDK ${AppInfo.sdkVersion}${liteTxt} isEmbedded=${isEmbedded()}`);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
return (
<JitsiThemePaperProvider>
{ super.render() }
</JitsiThemePaperProvider>
);
}
/**
* Initializes feature flags and updates settings.
*
* @returns {void}
*/
async _extraInit() {
const { dispatch, getState } = this.state.store ?? {};
const { flags = {}, url, userInfo } = this.props;
let callIntegrationEnabled = flags[CALL_INTEGRATION_ENABLED as keyof typeof flags];
// CallKit does not work on the simulator, make sure we disable it.
if (Platform.OS === 'ios' && DeviceInfo.isEmulatorSync()) {
flags[CALL_INTEGRATION_ENABLED] = false;
callIntegrationEnabled = false;
logger.info('Disabling CallKit because this is a simulator');
}
// Disable Android ConnectionService by default.
if (Platform.OS === 'android' && typeof callIntegrationEnabled === 'undefined') {
flags[CALL_INTEGRATION_ENABLED] = false;
callIntegrationEnabled = false;
}
// We set these early enough so then we avoid any unnecessary re-renders.
dispatch?.(updateFlags(flags));
const route = await _getRouteToRender();
// We need the root navigator to be set early.
await this._navigate(route);
// HACK ALERT!
// Wait until the root navigator is ready.
// We really need to break the inheritance relationship between App,
// AbstractApp and BaseApp, it's very inflexible and cumbersome right now.
const rootNavigationReady = new Promise<void>(resolve => {
const i = setInterval(() => {
// @ts-ignore
const { ready } = getState()['features/app'] || {};
if (ready) {
clearInterval(i);
resolve();
}
}, 50);
});
await rootNavigationReady;
// Update specified server URL.
if (typeof url !== 'undefined') {
// @ts-ignore
const { serverURL } = url;
if (typeof serverURL !== 'undefined') {
dispatch?.(updateSettings({ serverURL }));
}
}
// @ts-ignore
dispatch?.(updateSettings(userInfo || {}));
// Update settings with feature-flag.
if (typeof callIntegrationEnabled !== 'undefined') {
dispatch?.(updateSettings({ disableCallIntegration: !callIntegrationEnabled }));
}
}
/**
* Overrides the parent method to inject {@link DimensionsDetector} as
* the top most component.
*
* @override
*/
_createMainElement(component: ComponentType<any>, props: Object) {
return (
<SafeAreaProvider>
<DimensionsDetector
onDimensionsChanged = { this._onDimensionsChanged }
onSafeAreaInsetsChanged = { this._onSafeAreaInsetsChanged }>
{ super._createMainElement(component, props) }
</DimensionsDetector>
</SafeAreaProvider>
);
}
/**
* Attempts to disable the use of React Native
* {@link ExceptionsManager#handleException} on platforms and in
* configurations on/in which the use of the method in questions has been
* determined to be undesirable. For example, React Native will
* (intentionally) throw an unhandled {@code JavascriptException} for an
* unhandled JavaScript error in the Release configuration. This will
* effectively kill the app. In accord with the Web, do not kill the app.
*
* @private
* @returns {void}
*/
_maybeDisableExceptionsManager() {
if (__DEV__) {
// As mentioned above, only the Release configuration was observed
// to suffer.
return;
}
if (Platform.OS !== 'android') {
// A solution based on RTCSetFatalHandler was implemented on iOS and
// it is preferred because it is at a later step of the
// error/exception handling and it is specific to fatal
// errors/exceptions which were observed to kill the app. The
// solution implemented below was tested on Android only so it is
// considered safest to use it there only.
return;
}
// @ts-ignore
const oldHandler = global.ErrorUtils.getGlobalHandler();
const newHandler = _handleException;
if (!oldHandler || oldHandler !== newHandler) {
// @ts-ignore
newHandler.next = oldHandler;
// @ts-ignore
global.ErrorUtils.setGlobalHandler(newHandler);
}
}
/**
* Updates the known available size for the app to occupy.
*
* @param {number} width - The component's current width.
* @param {number} height - The component's current height.
* @private
* @returns {void}
*/
_onDimensionsChanged(width: number, height: number) {
const { dispatch } = this.state.store ?? {};
dispatch?.(clientResized(width, height));
}
/**
* Updates the safe are insets values.
*
* @param {Object} insets - The insets.
* @param {number} insets.top - The top inset.
* @param {number} insets.right - The right inset.
* @param {number} insets.bottom - The bottom inset.
* @param {number} insets.left - The left inset.
* @private
* @returns {void}
*/
_onSafeAreaInsetsChanged(insets: Object) {
const { dispatch } = this.state.store ?? {};
dispatch?.(setSafeAreaInsets(insets));
}
/**
* Renders the platform specific dialog container.
*
* @returns {React$Element}
*/
_renderDialogContainer() {
return (
<DialogContainerWrapper
pointerEvents = 'box-none'
style = { StyleSheet.absoluteFill }>
<BottomSheetContainer />
<DialogContainer />
</DialogContainerWrapper>
);
}
}
/**
* Handles a (possibly unhandled) JavaScript error by preventing React Native
* from converting a fatal error into an unhandled native exception which will
* kill the app.
*
* @param {Error} error - The (possibly unhandled) JavaScript error to handle.
* @param {boolean} fatal - If the specified error is fatal, {@code true};
* otherwise, {@code false}.
* @private
* @returns {void}
*/
function _handleException(error: Error, fatal: boolean) {
if (fatal) {
// In the Release configuration, React Native will (intentionally) throw
// an unhandled JavascriptException for an unhandled JavaScript error.
// This will effectively kill the app. In accord with the Web, do not
// kill the app.
logger.error(error);
} else {
// Forward to the next globalHandler of ErrorUtils.
// @ts-ignore
const { next } = _handleException;
typeof next === 'function' && next(error, fatal);
}
}

View File

@@ -0,0 +1,67 @@
import React from 'react';
import GlobalStyles from '../../base/ui/components/GlobalStyles.web';
import JitsiThemeProvider from '../../base/ui/components/JitsiThemeProvider.web';
import DialogContainer from '../../base/ui/components/web/DialogContainer';
import ChromeExtensionBanner from '../../chrome-extension-banner/components/ChromeExtensionBanner.web';
import OverlayContainer from '../../overlay/components/web/OverlayContainer';
import { AbstractApp } from './AbstractApp';
// Register middlewares and reducers.
import '../middlewares';
import '../reducers';
/**
* Root app {@code Component} on Web/React.
*
* @augments AbstractApp
*/
export class App extends AbstractApp {
/**
* Creates an extra {@link ReactElement}s to be added (unconditionally)
* alongside the main element.
*
* @abstract
* @protected
* @returns {ReactElement}
*/
override _createExtraElement() {
return (
<JitsiThemeProvider>
<OverlayContainer />
</JitsiThemeProvider>
);
}
/**
* Overrides the parent method to inject {@link AtlasKitThemeProvider} as
* the top most component.
*
* @override
*/
override _createMainElement(component: React.ComponentType, props?: Object) {
return (
<JitsiThemeProvider>
<GlobalStyles />
<ChromeExtensionBanner />
{ super._createMainElement(component, props) }
</JitsiThemeProvider>
);
}
/**
* Renders the platform specific dialog container.
*
* @returns {React$Element}
*/
override _renderDialogContainer() {
return (
<JitsiThemeProvider>
<DialogContainer />
</JitsiThemeProvider>
);
}
}

View File

@@ -0,0 +1,25 @@
import { IStateful } from '../base/app/types';
import { MEDIA_TYPE } from '../base/media/constants';
import { toState } from '../base/redux/functions';
import { isLocalTrackMuted } from '../base/tracks/functions';
import { addHashParamsToURL } from '../base/util/uri';
/**
* Adds the current track state to the passed URL.
*
* @param {URL} url - The URL that will be modified.
* @param {Function|Object} stateful - The redux store or {@code getState} function.
* @returns {URL} - Returns the modified URL.
*/
export function addTrackStateToURL(url: string, stateful: IStateful) {
const state = toState(stateful);
const tracks = state['features/base/tracks'];
const isVideoMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO);
const isAudioMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
return addHashParamsToURL(new URL(url), { // use new URL object in order to not pollute the passed parameter.
'config.startWithAudioMuted': isAudioMuted,
'config.startWithVideoMuted': isVideoMuted
});
}

View File

@@ -0,0 +1,40 @@
import { NativeModules } from 'react-native';
import { IStateful } from '../base/app/types';
import { toState } from '../base/redux/functions';
import { getServerURL } from '../base/settings/functions.native';
export * from './functions.any';
/**
* Retrieves the default URL for the app. This can either come from a prop to
* the root App component or be configured in the settings.
*
* @param {Function|Object} stateful - The redux store or {@code getState}
* function.
* @returns {string} - Default URL for the app.
*/
export function getDefaultURL(stateful: IStateful) {
const state = toState(stateful);
return getServerURL(state);
}
/**
* Returns application name.
*
* @returns {string} The application name.
*/
export function getName() {
return NativeModules.AppInfo.name;
}
/**
* Returns the path to the Jitsi Meet SDK bundle on iOS. On Android it will be
* undefined.
*
* @returns {string|undefined}
*/
export function getSdkBundlePath() {
return NativeModules.AppInfo.sdkBundlePath;
}

View File

@@ -0,0 +1,59 @@
import { IStateful } from '../base/app/types';
import { toState } from '../base/redux/functions';
import { getServerURL } from '../base/settings/functions.web';
import { getJitsiMeetGlobalNS } from '../base/util/helpers';
export * from './functions.any';
import logger from './logger';
/**
* Retrieves the default URL for the app. This can either come from a prop to
* the root App component or be configured in the settings.
*
* @param {Function|Object} stateful - The redux store or {@code getState}
* function.
* @returns {string} - Default URL for the app.
*/
export function getDefaultURL(stateful: IStateful) {
const state = toState(stateful);
const { href } = window.location;
if (href) {
return href;
}
return getServerURL(state);
}
/**
* Returns application name.
*
* @returns {string} The application name.
*/
export function getName() {
return interfaceConfig.APP_NAME;
}
/**
* Executes a handler function after the window load event has been received.
* If the app has already loaded, the handler is executed immediately.
* Otherwise, the handler is registered as a 'load' event listener.
*
* @param {Function} handler - The callback function to execute.
* @returns {void}
*/
export function executeAfterLoad(handler: () => void) {
const safeHandler = () => {
try {
handler();
} catch (error) {
logger.error('Error executing handler after load:', error);
}
};
if (getJitsiMeetGlobalNS()?.hasLoaded) {
safeHandler();
} else {
window.addEventListener('load', safeHandler);
}
}

View File

@@ -0,0 +1,18 @@
import RootNavigationContainer from '../mobile/navigation/components/RootNavigationContainer';
const route = {
component: RootNavigationContainer,
href: undefined
};
/**
* Determines which route is to be rendered in order to depict a specific Redux
* store.
*
* @param {any} _stateful - Used on web.
* @returns {Promise<Object>}
*/
export function _getRouteToRender(_stateful?: any) {
return Promise.resolve(route);
}

View File

@@ -0,0 +1,148 @@
// @ts-expect-error
import { generateRoomWithoutSeparator } from '@jitsi/js-utils/random';
import { getTokenAuthUrl } from '../authentication/functions.web';
import { IStateful } from '../base/app/types';
import { isRoomValid } from '../base/conference/functions';
import { isSupportedBrowser } from '../base/environment/environment';
import { browser } from '../base/lib-jitsi-meet';
import { toState } from '../base/redux/functions';
import { parseURIString } from '../base/util/uri';
import Conference from '../conference/components/web/Conference';
import { getDeepLinkingPage } from '../deep-linking/functions';
import UnsupportedDesktopBrowser from '../unsupported-browser/components/UnsupportedDesktopBrowser';
import BlankPage from '../welcome/components/BlankPage.web';
import WelcomePage from '../welcome/components/WelcomePage.web';
import { getCustomLandingPageURL, isWelcomePageEnabled } from '../welcome/functions';
import { IReduxState } from './types';
/**
* Determines which route is to be rendered in order to depict a specific Redux
* store.
*
* @param {(Function|Object)} stateful - THe redux store, state, or
* {@code getState} function.
* @returns {Promise<Object>}
*/
export function _getRouteToRender(stateful: IStateful) {
const state = toState(stateful);
return _getWebConferenceRoute(state) || _getWebWelcomePageRoute(state);
}
/**
* Returns the {@code Route} to display when trying to access a conference if
* a valid conference is being joined.
*
* @param {Object} state - The redux state.
* @returns {Promise|undefined}
*/
function _getWebConferenceRoute(state: IReduxState) {
const room = state['features/base/conference'].room;
if (!isRoomValid(room)) {
return;
}
const route = _getEmptyRoute();
const config = state['features/base/config'];
// if we have auto redirect enabled, and we have previously logged in successfully
// let's redirect to the auth url to get the token and login again
if (!browser.isElectron() && config.tokenAuthUrl && config.tokenAuthUrlAutoRedirect
&& state['features/authentication'].tokenAuthUrlSuccessful
&& !state['features/base/jwt'].jwt && room) {
const { locationURL = { href: '' } as URL } = state['features/base/connection'];
const { tenant } = parseURIString(locationURL.href) || {};
const { startAudioOnly } = config;
return getTokenAuthUrl(
config,
locationURL,
{
audioMuted: false,
audioOnlyEnabled: startAudioOnly,
skipPrejoin: false,
videoMuted: false
},
room,
tenant
)
.then((url: string | undefined) => {
route.href = url;
return route;
})
.catch(() => Promise.resolve(route));
}
// Update the location if it doesn't match. This happens when a room is
// joined from the welcome page. The reason for doing this instead of using
// the history API is that we want to load the config.js which takes the
// room into account.
const { locationURL } = state['features/base/connection'];
if (window.location.href !== locationURL?.href) {
route.href = locationURL?.href;
return Promise.resolve(route);
}
return getDeepLinkingPage(state)
.then(deepLinkComponent => {
if (deepLinkComponent) {
route.component = deepLinkComponent;
} else if (isSupportedBrowser()) {
route.component = Conference;
} else {
route.component = UnsupportedDesktopBrowser;
}
return route;
});
}
/**
* Returns the {@code Route} to display when trying to access the welcome page.
*
* @param {Object} state - The redux state.
* @returns {Promise<Object>}
*/
function _getWebWelcomePageRoute(state: IReduxState) {
const route = _getEmptyRoute();
if (isWelcomePageEnabled(state)) {
if (isSupportedBrowser()) {
const customLandingPage = getCustomLandingPageURL(state);
if (customLandingPage) {
route.href = customLandingPage;
} else {
route.component = WelcomePage;
}
} else {
route.component = UnsupportedDesktopBrowser;
}
} else {
// Web: if the welcome page is disabled, go directly to a random room.
const url = new URL(window.location.href);
url.pathname += generateRoomWithoutSeparator();
route.href = url.href;
}
return Promise.resolve(route);
}
/**
* Returns the default {@code Route}.
*
* @returns {Object}
*/
function _getEmptyRoute(): { component: React.ReactNode; href?: string; } {
return {
component: BlankPage,
href: undefined
};
}

View File

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

View File

@@ -0,0 +1,180 @@
import { AnyAction } from 'redux';
import { createConnectionEvent } from '../analytics/AnalyticsEvents';
import { sendAnalytics } from '../analytics/functions';
import { appWillNavigate } from '../base/app/actions';
import { SET_ROOM } from '../base/conference/actionTypes';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../base/connection/actionTypes';
import { getURLWithoutParams } from '../base/connection/utils';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { isEmbedded } from '../base/util/embedUtils';
import { reloadNow } from './actions';
import { _getRouteToRender } from './getRouteToRender';
import { IStore } from './types';
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case CONNECTION_ESTABLISHED:
return _connectionEstablished(store, next, action);
case CONNECTION_FAILED:
return _connectionFailed(store, next, action);
case SET_ROOM:
return _setRoom(store, next, action);
}
return next(action);
});
/**
* Notifies the feature app that the action {@link CONNECTION_ESTABLISHED} is
* being dispatched within a specific redux {@code 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 new state that is the result of the reduction of the
* specified {@code action}.
*/
function _connectionEstablished(store: IStore, next: Function, action: AnyAction) {
const result = next(action);
// In the Web app we explicitly do not want to display the hash and
// query/search URL params. Unfortunately, window.location and, more
// importantly, its params are used not only in jitsi-meet but also in
// lib-jitsi-meet. Consequently, the time to remove the params is
// determined by when no one needs them anymore.
// @ts-ignore
const { history, location } = window;
if (isEmbedded()) {
return;
}
if (history
&& location
&& history.length
&& typeof history.replaceState === 'function') {
// @ts-ignore
const replacement = getURLWithoutParams(location);
// @ts-ignore
if (location !== replacement) {
history.replaceState(
history.state,
document?.title || '',
replacement);
}
}
return result;
}
/**
* CONNECTION_FAILED action side effects.
*
* @param {Object} store - The 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 CONNECTION_FAILED} which is being dispatched in the specified
* {@code store}.
* @returns {Object}
* @private
*/
function _connectionFailed({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
// In the case of a split-brain error, reload early and prevent further
// handling of the action.
if (_isMaybeSplitBrainError(getState, action)) {
dispatch(reloadNow());
return;
}
return next(action);
}
/**
* Returns whether or not a CONNECTION_FAILED action is for a possible split brain error. A split brain error occurs
* when at least two users join a conference on different bridges. It is assumed the split brain scenario occurs very
* early on in the call.
*
* @param {Function} getState - The redux function for fetching the current state.
* @param {Action} action - The redux action {@code CONNECTION_FAILED} which is being dispatched in the specified
* {@code store}.
* @private
* @returns {boolean}
*/
function _isMaybeSplitBrainError(getState: IStore['getState'], action: AnyAction) {
const { error } = action;
const isShardChangedError = error
&& error.message === 'item-not-found'
&& error.details?.shard_changed;
if (isShardChangedError) {
const state = getState();
const { timeEstablished } = state['features/base/connection'];
const { _immediateReloadThreshold } = state['features/base/config'];
const timeSinceConnectionEstablished = Number(timeEstablished && Date.now() - timeEstablished);
const reloadThreshold = typeof _immediateReloadThreshold === 'number' ? _immediateReloadThreshold : 1500;
const isWithinSplitBrainThreshold = !timeEstablished || timeSinceConnectionEstablished <= reloadThreshold;
sendAnalytics(createConnectionEvent('failed', {
...error,
connectionEstablished: timeEstablished,
splitBrain: isWithinSplitBrainThreshold,
timeSinceConnectionEstablished
}));
return isWithinSplitBrainThreshold;
}
return false;
}
/**
* Navigates to a route in accord with a specific redux state.
*
* @param {Store} store - The redux store which determines/identifies the route
* to navigate to.
* @private
* @returns {void}
*/
function _navigate({ dispatch, getState }: IStore) {
const state = getState();
const { app } = state['features/base/app'];
_getRouteToRender(state).then((route: Object) => {
dispatch(appWillNavigate(app, route));
return app._navigate(route);
});
}
/**
* Notifies the feature app that the action {@link SET_ROOM} is being dispatched
* within a specific redux {@code 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 new state that is the result of the reduction of the
* specified {@code action}.
*/
function _setRoom(store: IStore, next: Function, action: AnyAction) {
const result = next(action);
_navigate(store);
return result;
}

View File

@@ -0,0 +1,57 @@
import '../analytics/middleware';
import '../authentication/middleware';
import '../av-moderation/middleware';
import '../base/conference/middleware';
import '../base/config/middleware';
import '../base/i18n/middleware';
import '../base/jwt/middleware';
import '../base/known-domains/middleware';
import '../base/lastn/middleware';
import '../base/lib-jitsi-meet/middleware';
import '../base/logging/middleware';
import '../base/media/middleware';
import '../base/net-info/middleware';
import '../base/participants/middleware';
import '../base/responsive-ui/middleware';
import '../base/redux/middleware';
import '../base/settings/middleware';
import '../base/sounds/middleware';
import '../base/testing/middleware';
import '../base/tracks/middleware';
import '../base/user-interaction/middleware';
import '../breakout-rooms/middleware';
import '../calendar-sync/middleware';
import '../chat/middleware';
import '../conference/middleware';
import '../connection-indicator/middleware';
import '../device-selection/middleware';
import '../display-name/middleware';
import '../dynamic-branding/middleware';
import '../etherpad/middleware';
import '../filmstrip/middleware';
import '../follow-me/middleware';
import '../invite/middleware';
import '../jaas/middleware';
import '../large-video/middleware';
import '../lobby/middleware';
import '../notifications/middleware';
import '../overlay/middleware';
import '../participants-pane/middleware';
import '../polls/middleware';
import '../polls-history/middleware';
import '../reactions/middleware';
import '../recent-list/middleware';
import '../recording/middleware';
import '../rejoin/middleware';
import '../room-lock/middleware';
import '../rtcstats/middleware';
import '../speaker-stats/middleware';
import '../subtitles/middleware';
import '../transcribing/middleware';
import '../video-layout/middleware';
import '../video-quality/middleware';
import '../videosipgw/middleware';
import '../visitors/middleware';
import '../whiteboard/middleware.any';
import './middleware';

View File

@@ -0,0 +1,19 @@
import '../dynamic-branding/middleware';
import '../gifs/middleware';
import '../mobile/audio-mode/middleware';
import '../mobile/background/middleware';
import '../mobile/call-integration/middleware';
import '../mobile/external-api/middleware';
import '../mobile/full-screen/middleware';
import '../mobile/navigation/middleware';
import '../mobile/permissions/middleware';
import '../mobile/proximity/middleware';
import '../mobile/wake-lock/middleware';
import '../mobile/react-native-sdk/middleware';
import '../mobile/watchos/middleware';
import '../share-room/middleware';
import '../shared-video/middleware';
import '../toolbox/middleware.native';
import '../whiteboard/middleware.native';
import './middlewares.any';

View File

@@ -0,0 +1,28 @@
import '../base/app/middleware';
import '../base/connection/middleware';
import '../base/devices/middleware';
import '../base/media/middleware';
import '../deep-linking/middleware.web';
import '../dynamic-branding/middleware';
import '../e2ee/middleware';
import '../external-api/middleware';
import '../keyboard-shortcuts/middleware';
import '../no-audio-signal/middleware';
import '../notifications/middleware';
import '../noise-detection/middleware';
import '../old-client-notification/middleware';
import '../power-monitor/middleware';
import '../prejoin/middleware';
import '../remote-control/middleware';
import '../screen-share/middleware';
import '../shared-video/middleware';
import '../web-hid/middleware';
import '../settings/middleware';
import '../talk-while-muted/middleware';
import '../toolbox/middleware';
import '../face-landmarks/middleware';
import '../gifs/middleware';
import '../whiteboard/middleware.web';
import '../file-sharing/middleware.web';
import './middlewares.any';

View File

@@ -0,0 +1,22 @@
import ReducerRegistry from '../base/redux/ReducerRegistry';
import { _ROOT_NAVIGATION_READY } from '../mobile/navigation/actionTypes';
/**
* Listen for actions which changes the state of the app feature.
*
* @param {Object} state - The Redux state of the feature features/app.
* @param {Object} action - Action object.
* @param {string} action.type - Type of action.
* @returns {Object}
*/
ReducerRegistry.register('features/app', (state: Object = {}, action) => {
switch (action.type) {
case _ROOT_NAVIGATION_READY:
return {
...state,
ready: action.ready
};
default:
return state;
}
});

View File

@@ -0,0 +1,58 @@
import '../analytics/reducer';
import '../authentication/reducer';
import '../av-moderation/reducer';
import '../base/app/reducer';
import '../base/audio-only/reducer';
import '../base/conference/reducer';
import '../base/config/reducer';
import '../base/connection/reducer';
import '../base/dialog/reducer';
import '../base/flags/reducer';
import '../base/jwt/reducer';
import '../base/known-domains/reducer';
import '../base/lastn/reducer';
import '../base/lib-jitsi-meet/reducer';
import '../base/logging/reducer';
import '../base/media/reducer';
import '../base/net-info/reducer';
import '../base/participants/reducer';
import '../base/responsive-ui/reducer';
import '../base/settings/reducer';
import '../base/sounds/reducer';
import '../base/testing/reducer';
import '../base/tracks/reducer';
import '../base/user-interaction/reducer';
import '../breakout-rooms/reducer';
import '../calendar-sync/reducer';
import '../chat/reducer';
import '../deep-linking/reducer';
import '../dropbox/reducer';
import '../dynamic-branding/reducer';
import '../etherpad/reducer';
import '../filmstrip/reducer';
import '../follow-me/reducer';
import '../gifs/reducer';
import '../google-api/reducer';
import '../invite/reducer';
import '../jaas/reducer';
import '../large-video/reducer';
import '../lobby/reducer';
import '../notifications/reducer';
import '../participants-pane/reducer';
import '../polls/reducer';
import '../polls-history/reducer';
import '../reactions/reducer';
import '../recent-list/reducer';
import '../recording/reducer';
import '../settings/reducer';
import '../speaker-stats/reducer';
import '../shared-video/reducer';
import '../subtitles/reducer';
import '../screen-share/reducer';
import '../toolbox/reducer';
import '../transcribing/reducer';
import '../video-layout/reducer';
import '../video-quality/reducer';
import '../videosipgw/reducer';
import '../visitors/reducer';
import '../whiteboard/reducer';

View File

@@ -0,0 +1,11 @@
import '../mobile/audio-mode/reducer';
import '../mobile/background/reducer';
import '../mobile/call-integration/reducer';
import '../mobile/external-api/reducer';
import '../mobile/full-screen/reducer';
import '../mobile/watchos/reducer';
import '../share-room/reducer';
import './reducer.native';
import './reducers.any';

View File

@@ -0,0 +1,22 @@
import '../base/devices/reducer';
import '../base/premeeting/reducer';
import '../base/tooltip/reducer';
import '../e2ee/reducer';
import '../face-landmarks/reducer';
import '../feedback/reducer';
import '../keyboard-shortcuts/reducer';
import '../no-audio-signal/reducer';
import '../noise-detection/reducer';
import '../participants-pane/reducer';
import '../power-monitor/reducer';
import '../prejoin/reducer';
import '../remote-control/reducer';
import '../screen-share/reducer';
import '../noise-suppression/reducer';
import '../screenshot-capture/reducer';
import '../talk-while-muted/reducer';
import '../virtual-background/reducer';
import '../web-hid/reducer';
import '../file-sharing/reducer';
import './reducers.any';

183
react/features/app/types.ts Normal file
View File

@@ -0,0 +1,183 @@
import { AnyAction } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { IAnalyticsState } from '../analytics/reducer';
import { IAuthenticationState } from '../authentication/reducer';
import { IAVModerationState } from '../av-moderation/reducer';
import { IAppState } from '../base/app/reducer';
import { IAudioOnlyState } from '../base/audio-only/reducer';
import { IConferenceState } from '../base/conference/reducer';
import { IConfigState } from '../base/config/reducer';
import { IConnectionState } from '../base/connection/reducer';
import { IDevicesState } from '../base/devices/types';
import { IDialogState } from '../base/dialog/reducer';
import { IFlagsState } from '../base/flags/reducer';
import { IJwtState } from '../base/jwt/reducer';
import { IKnownDomainsState } from '../base/known-domains/reducer';
import { ILastNState } from '../base/lastn/reducer';
import { ILibJitsiMeetState } from '../base/lib-jitsi-meet/reducer';
import { ILoggingState } from '../base/logging/reducer';
import { IMediaState } from '../base/media/reducer';
import { INetInfoState } from '../base/net-info/reducer';
import { IParticipantsState } from '../base/participants/reducer';
import { IPreMeetingState } from '../base/premeeting/types';
import { IResponsiveUIState } from '../base/responsive-ui/reducer';
import { ISettingsState } from '../base/settings/reducer';
import { ISoundsState } from '../base/sounds/reducer';
import { ITestingState } from '../base/testing/reducer';
import { ITooltipState } from '../base/tooltip/reducer';
import { INoSrcDataState, ITracksState } from '../base/tracks/reducer';
import { IUserInteractionState } from '../base/user-interaction/reducer';
import { IBreakoutRoomsState } from '../breakout-rooms/reducer';
import { ICalendarSyncState } from '../calendar-sync/reducer';
import { IChatState } from '../chat/reducer';
import { IDeepLinkingState } from '../deep-linking/reducer';
import { IDropboxState } from '../dropbox/reducer';
import { IDynamicBrandingState } from '../dynamic-branding/reducer';
import { IE2EEState } from '../e2ee/reducer';
import { IEtherpadState } from '../etherpad/reducer';
import { IFaceLandmarksState } from '../face-landmarks/reducer';
import { IFeedbackState } from '../feedback/reducer';
import { IFileSharingState } from '../file-sharing/reducer';
import { IFilmstripState } from '../filmstrip/reducer';
import { IFollowMeState } from '../follow-me/reducer';
import { IGifsState } from '../gifs/reducer';
import { IGoogleApiState } from '../google-api/reducer';
import { IInviteState } from '../invite/reducer';
import { IJaaSState } from '../jaas/reducer';
import { IKeyboardShortcutsState } from '../keyboard-shortcuts/types';
import { ILargeVideoState } from '../large-video/reducer';
import { ILobbyState } from '../lobby/reducer';
import { IMobileAudioModeState } from '../mobile/audio-mode/reducer';
import { IMobileBackgroundState } from '../mobile/background/reducer';
import { ICallIntegrationState } from '../mobile/call-integration/reducer';
import { IMobileExternalApiState } from '../mobile/external-api/reducer';
import { IFullScreenState } from '../mobile/full-screen/reducer';
import { IMobileWatchOSState } from '../mobile/watchos/reducer';
import { INoAudioSignalState } from '../no-audio-signal/reducer';
import { INoiseDetectionState } from '../noise-detection/reducer';
import { INoiseSuppressionState } from '../noise-suppression/reducer';
import { INotificationsState } from '../notifications/reducer';
import { IParticipantsPaneState } from '../participants-pane/reducer';
import { IPollsState } from '../polls/reducer';
import { IPollsHistoryState } from '../polls-history/reducer';
import { IPowerMonitorState } from '../power-monitor/reducer';
import { IPrejoinState } from '../prejoin/reducer';
import { IReactionsState } from '../reactions/reducer';
import { IRecentListState } from '../recent-list/reducer';
import { IRecordingState } from '../recording/reducer';
import { IRemoteControlState } from '../remote-control/reducer';
import { IScreenShareState } from '../screen-share/reducer';
import { IScreenshotCaptureState } from '../screenshot-capture/reducer';
import { IShareRoomState } from '../share-room/reducer';
import { ISharedVideoState } from '../shared-video/reducer';
import { ISpeakerStatsState } from '../speaker-stats/reducer';
import { ISubtitlesState } from '../subtitles/reducer';
import { ITalkWhileMutedState } from '../talk-while-muted/reducer';
import { IToolboxState } from '../toolbox/reducer';
import { ITranscribingState } from '../transcribing/reducer';
import { IVideoLayoutState } from '../video-layout/reducer';
import { IVideoQualityPersistedState, IVideoQualityState } from '../video-quality/reducer';
import { IVideoSipGW } from '../videosipgw/reducer';
import { IVirtualBackground } from '../virtual-background/reducer';
import { IVisitorsState } from '../visitors/reducer';
import { IWebHid } from '../web-hid/reducer';
import { IWhiteboardState } from '../whiteboard/reducer';
export interface IStore {
dispatch: ThunkDispatch<IReduxState, void, AnyAction>;
getState: () => IReduxState;
}
export interface IReduxState {
'features/analytics': IAnalyticsState;
'features/authentication': IAuthenticationState;
'features/av-moderation': IAVModerationState;
'features/base/app': IAppState;
'features/base/audio-only': IAudioOnlyState;
'features/base/color-scheme': any;
'features/base/conference': IConferenceState;
'features/base/config': IConfigState;
'features/base/connection': IConnectionState;
'features/base/devices': IDevicesState;
'features/base/dialog': IDialogState;
'features/base/flags': IFlagsState;
'features/base/jwt': IJwtState;
'features/base/known-domains': IKnownDomainsState;
'features/base/lastn': ILastNState;
'features/base/lib-jitsi-meet': ILibJitsiMeetState;
'features/base/logging': ILoggingState;
'features/base/media': IMediaState;
'features/base/net-info': INetInfoState;
'features/base/no-src-data': INoSrcDataState;
'features/base/participants': IParticipantsState;
'features/base/premeeting': IPreMeetingState;
'features/base/responsive-ui': IResponsiveUIState;
'features/base/settings': ISettingsState;
'features/base/sounds': ISoundsState;
'features/base/tooltip': ITooltipState;
'features/base/tracks': ITracksState;
'features/base/user-interaction': IUserInteractionState;
'features/breakout-rooms': IBreakoutRoomsState;
'features/calendar-sync': ICalendarSyncState;
'features/call-integration': ICallIntegrationState;
'features/chat': IChatState;
'features/deep-linking': IDeepLinkingState;
'features/dropbox': IDropboxState;
'features/dynamic-branding': IDynamicBrandingState;
'features/e2ee': IE2EEState;
'features/etherpad': IEtherpadState;
'features/face-landmarks': IFaceLandmarksState;
'features/feedback': IFeedbackState;
'features/file-sharing': IFileSharingState;
'features/filmstrip': IFilmstripState;
'features/follow-me': IFollowMeState;
'features/full-screen': IFullScreenState;
'features/gifs': IGifsState;
'features/google-api': IGoogleApiState;
'features/invite': IInviteState;
'features/jaas': IJaaSState;
'features/keyboard-shortcuts': IKeyboardShortcutsState;
'features/large-video': ILargeVideoState;
'features/lobby': ILobbyState;
'features/mobile/audio-mode': IMobileAudioModeState;
'features/mobile/background': IMobileBackgroundState;
'features/mobile/external-api': IMobileExternalApiState;
'features/mobile/watchos': IMobileWatchOSState;
'features/no-audio-signal': INoAudioSignalState;
'features/noise-detection': INoiseDetectionState;
'features/noise-suppression': INoiseSuppressionState;
'features/notifications': INotificationsState;
'features/participants-pane': IParticipantsPaneState;
'features/polls': IPollsState;
'features/polls-history': IPollsHistoryState;
'features/power-monitor': IPowerMonitorState;
'features/prejoin': IPrejoinState;
'features/reactions': IReactionsState;
'features/recent-list': IRecentListState;
'features/recording': IRecordingState;
'features/remote-control': IRemoteControlState;
'features/screen-share': IScreenShareState;
'features/screenshot-capture': IScreenshotCaptureState;
'features/settings': ISettingsState;
'features/share-room': IShareRoomState;
'features/shared-video': ISharedVideoState;
'features/speaker-stats': ISpeakerStatsState;
'features/subtitles': ISubtitlesState;
'features/talk-while-muted': ITalkWhileMutedState;
'features/testing': ITestingState;
'features/toolbox': IToolboxState;
'features/transcribing': ITranscribingState;
'features/video-layout': IVideoLayoutState;
'features/video-quality': IVideoQualityState;
'features/video-quality-persistent-storage': IVideoQualityPersistedState;
'features/videosipgw': IVideoSipGW;
'features/virtual-background': IVirtualBackground;
'features/visitors': IVisitorsState;
'features/web-hid': IWebHid;
'features/whiteboard': IWhiteboardState;
}
export interface IReloadNowOptions {
hidePrejoin?: boolean;
}