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,18 @@
/**
* The type of the action which signals to open the conference in the desktop
* app.
*
* {
* type: OPEN_DESKTOP
* }
*/
export const OPEN_DESKTOP_APP = 'OPEN_DESKTOP_APP';
/**
* The type of the action which signals to open the conference in the web app.
*
* {
* type: OPEN_WEB_APP
* }
*/
export const OPEN_WEB_APP = 'OPEN_WEB_APP';

View File

@@ -0,0 +1,33 @@
import { appNavigate } from '../app/actions';
import { IStore } from '../app/types';
import { OPEN_DESKTOP_APP, OPEN_WEB_APP } from './actionTypes';
/**
* Continue to the conference page.
*
* @returns {Function}
*/
export function openWebApp() {
return (dispatch: IStore['dispatch']) => {
// In order to go to the web app we need to skip the deep linking
// interceptor. OPEN_WEB_APP action should set launchInWeb to true in
// the redux store. After this when appNavigate() is called the
// deep linking interceptor will be skipped (will return undefined).
dispatch({ type: OPEN_WEB_APP });
dispatch(appNavigate());
};
}
/**
* Opens the desktop app.
*
* @returns {{
* type: OPEN_DESKTOP_APP
* }}
*/
export function openDesktopApp() {
return {
type: OPEN_DESKTOP_APP
};
}

View File

@@ -0,0 +1 @@
export default {};

View File

@@ -0,0 +1,177 @@
import { Theme } from '@mui/material';
import React, { useCallback, useEffect } from 'react';
import { WithTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { createDeepLinkingPageEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState } from '../../app/types';
import { IDeeplinkingConfig } from '../../base/config/configType';
import { getLegalUrls } from '../../base/config/functions.any';
import { isSupportedBrowser } from '../../base/environment/environment';
import { translate, translateToHTML } from '../../base/i18n/functions';
import Platform from '../../base/react/Platform.web';
import Button from '../../base/ui/components/web/Button';
import { BUTTON_TYPES } from '../../base/ui/constants.any';
import {
openDesktopApp,
openWebApp
} from '../actions';
import { _TNS } from '../constants';
const useStyles = makeStyles()((theme: Theme) => {
return {
container: {
background: '#1E1E1E',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
display: 'flex'
},
contentPane: {
display: 'flex',
flexDirection: 'column',
background: theme.palette.ui01,
border: `1px solid ${theme.palette.ui03}`,
padding: 40,
borderRadius: 16,
maxWidth: 410,
color: theme.palette.text01
},
logo: {
marginBottom: 32
},
launchingMeetingLabel: {
marginBottom: 16,
...theme.typography.heading4
},
roomName: {
marginBottom: 32,
...theme.typography.heading5
},
descriptionLabel: {
marginBottom: 32,
...theme.typography.bodyLongRegular
},
buttonsContainer: {
display: 'flex',
justifyContent: 'flex-start',
'& > *:not(:last-child)': {
marginRight: 16
}
},
separator: {
marginTop: 40,
height: 1,
maxWidth: 390,
background: theme.palette.ui03
},
label: {
marginTop: 40,
...theme.typography.labelRegular,
color: theme.palette.text02,
'& a': {
color: theme.palette.link01
}
}
};
});
const DeepLinkingDesktopPage: React.FC<WithTranslation> = ({ t }) => {
const dispatch = useDispatch();
const room = useSelector((state: IReduxState) => decodeURIComponent(state['features/base/conference'].room || ''));
const deeplinkingCfg = useSelector((state: IReduxState) =>
state['features/base/config']?.deeplinking || {} as IDeeplinkingConfig);
const generateDownloadURL = useCallback(() => {
const downloadCfg = deeplinkingCfg.desktop?.download;
if (downloadCfg) {
return downloadCfg[Platform.OS as keyof typeof downloadCfg];
}
}, [ deeplinkingCfg ]);
const legalUrls = useSelector(getLegalUrls);
const { hideLogo, desktop } = deeplinkingCfg;
const { classes: styles } = useStyles();
const onLaunchWeb = useCallback(() => {
sendAnalytics(
createDeepLinkingPageEvent(
'clicked', 'launchWebButton', { isMobileBrowser: false }));
dispatch(openWebApp());
}, []);
const onTryAgain = useCallback(() => {
sendAnalytics(
createDeepLinkingPageEvent(
'clicked', 'tryAgainButton', { isMobileBrowser: false }));
dispatch(openDesktopApp());
}, []);
useEffect(() => {
sendAnalytics(
createDeepLinkingPageEvent(
'displayed', 'DeepLinkingDesktop', { isMobileBrowser: false }));
}, []);
return (
<div className = { styles.container }>
<div className = { styles.contentPane }>
<div className = 'header'>
{
!hideLogo
&& <img
alt = { t('welcomepage.logo.logoDeepLinking') }
className = { styles.logo }
src = 'images/logo-deep-linking.png' />
}
</div>
<div className = { styles.launchingMeetingLabel }>
{
t(`${_TNS}.titleNew`)
}
</div>
<div className = { styles.roomName }>{ room }</div>
<div className = { styles.descriptionLabel }>
{
isSupportedBrowser()
? translateToHTML(t, `${_TNS}.descriptionNew`, { app: desktop?.appName })
: t(`${_TNS}.descriptionWithoutWeb`, { app: desktop?.appName })
}
</div>
<div className = { styles.descriptionLabel }>
{
t(`${_TNS}.noDesktopApp`)
} &nbsp;
<a href = { generateDownloadURL() }>
{
t(`${_TNS}.downloadApp`)
}
</a>
</div>
<div className = { styles.buttonsContainer }>
<Button
label = { t(`${_TNS}.tryAgainButton`) }
onClick = { onTryAgain } />
{ isSupportedBrowser() && (
<Button
label = { t(`${_TNS}.launchWebButton`) }
onClick = { onLaunchWeb }
type = { BUTTON_TYPES.SECONDARY } />
)}
</div>
<div className = { styles.separator } />
<div className = { styles.label }> {translateToHTML(t, 'deepLinking.termsAndConditions', {
termsAndConditionsLink: legalUrls.terms
})}
</div>
</div>
</div>
);
};
export default translate(DeepLinkingDesktopPage);

View File

@@ -0,0 +1 @@
export default {};

View File

@@ -0,0 +1,219 @@
/* eslint-disable lines-around-comment */
import { Theme } from '@mui/material';
import React, { useCallback, useEffect, useMemo } from 'react';
import { WithTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { createDeepLinkingPageEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState } from '../../app/types';
import { IDeeplinkingConfig, IDeeplinkingMobileConfig } from '../../base/config/configType';
import { isSupportedMobileBrowser } from '../../base/environment/environment';
import { translate } from '../../base/i18n/functions';
import Platform from '../../base/react/Platform.web';
import Button from '../../base/ui/components/web/Button';
import DialInSummary from '../../invite/components/dial-in-summary/web/DialInSummary';
import { openWebApp } from '../actions';
import { _TNS } from '../constants';
import { generateDeepLinkingURL } from '../functions';
const PADDINGS = {
topBottom: 24,
leftRight: 40
};
const useStyles = makeStyles()((theme: Theme) => {
return {
container: {
background: '#1E1E1E',
width: '100vw',
height: '100dvh',
overflowX: 'hidden',
overflowY: 'auto',
justifyContent: 'center',
display: 'flex',
'& a': {
textDecoration: 'none'
}
},
contentPane: {
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
padding: `${PADDINGS.topBottom}px ${PADDINGS.leftRight}px`,
maxWidth: 410,
color: theme.palette.text01
},
launchingMeetingLabel: {
marginTop: 24,
textAlign: 'center',
marginBottom: 32,
...theme.typography.heading5
},
roomNameLabel: {
...theme.typography.bodyLongRegularLarge
},
joinMeetWrapper: {
marginTop: 24,
width: '100%'
},
labelDescription: {
textAlign: 'center',
marginTop: 16,
...theme.typography.bodyShortRegularLarge
},
linkWrapper: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginTop: 8,
width: '100%'
},
linkLabel: {
color: theme.palette.link01,
...theme.typography.bodyLongBoldLarge
},
supportedBrowserContent: {
marginTop: 16,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
},
labelOr: {
...theme.typography.bodyShortRegularLarge
},
separator: {
marginTop: '32px',
height: 1,
width: `calc(100% + ${2 * PADDINGS.leftRight}px)`,
background: theme.palette.ui03
}
};
});
const DeepLinkingMobilePage: React.FC<WithTranslation> = ({ t }) => {
const deeplinkingCfg = useSelector((state: IReduxState) =>
state['features/base/config']?.deeplinking || {} as IDeeplinkingConfig);
const { hideLogo } = deeplinkingCfg;
const deepLinkingUrl: string = useSelector(generateDeepLinkingURL);
const room = useSelector((state: IReduxState) => decodeURIComponent(state['features/base/conference'].room || ''));
const url = useSelector((state: IReduxState) => state['features/base/connection'] || {});
const dispatch = useDispatch();
const { classes: styles } = useStyles();
const generateDownloadURL = useCallback(() => {
const { downloadLink }
= (deeplinkingCfg?.[Platform.OS as keyof typeof deeplinkingCfg] || {}) as IDeeplinkingMobileConfig;
return downloadLink;
}, [ deeplinkingCfg ]);
const onDownloadApp = useCallback(() => {
sendAnalytics(
createDeepLinkingPageEvent(
'clicked', 'downloadAppButton', { isMobileBrowser: true }));
}, []);
const onLaunchWeb = useCallback(() => {
sendAnalytics(
createDeepLinkingPageEvent(
'clicked', 'launchWebButton', { isMobileBrowser: true }));
dispatch(openWebApp());
}, []);
const onOpenApp = useCallback(() => {
sendAnalytics(
createDeepLinkingPageEvent(
'clicked', 'openAppButton', { isMobileBrowser: true }));
}, []);
const onOpenLinkProperties = useMemo(() => {
const { downloadLink }
= (deeplinkingCfg?.[Platform.OS as keyof typeof deeplinkingCfg] || {}) as IDeeplinkingMobileConfig;
if (downloadLink) {
return {
// When opening a link to the download page, we want to let the
// OS itself handle intercepting and opening the appropriate
// app store. This avoids potential issues with browsers, such
// as iOS Chrome, not opening the store properly.
};
}
return {
// When falling back to another URL (Firebase) let the page be
// opened in a new window. This helps prevent the user getting
// trapped in an app-open-cycle where going back to the mobile
// browser re-triggers the app-open behavior.
target: '_blank',
rel: 'noopener noreferrer'
};
}, [ deeplinkingCfg ]);
useEffect(() => {
sendAnalytics(
createDeepLinkingPageEvent(
'displayed', 'DeepLinkingMobile', { isMobileBrowser: true }));
}, []);
return (
<div className = { styles.container }>
<div className = { styles.contentPane }>
{!hideLogo && (<img
alt = { t('welcomepage.logo.logoDeepLinking') }
src = 'images/logo-deep-linking-mobile.png' />
)}
<div className = { styles.launchingMeetingLabel }>{ t(`${_TNS}.launchMeetingLabel`) }</div>
<div className = ''>{room}</div>
<a
{ ...onOpenLinkProperties }
className = { styles.joinMeetWrapper }
href = { deepLinkingUrl }
onClick = { onOpenApp }
target = '_top'>
<Button
fullWidth = { true }
label = { t(`${_TNS}.joinInAppNew`) } />
</a>
<div className = { styles.labelDescription }>{ t(`${_TNS}.noMobileApp`) }</div>
<a
{ ...onOpenLinkProperties }
className = { styles.linkWrapper }
href = { generateDownloadURL() }
onClick = { onDownloadApp }
target = '_top'>
<div className = { styles.linkLabel }>{ t(`${_TNS}.downloadMobileApp`) }</div>
</a>
{isSupportedMobileBrowser() ? (
<div className = { styles.supportedBrowserContent }>
<div className = { styles.labelOr }>{ t(`${_TNS}.or`) }</div>
<a
className = { styles.linkWrapper }
onClick = { onLaunchWeb }
target = '_top'>
<div className = { styles.linkLabel }>{ t(`${_TNS}.joinInBrowser`) }</div>
</a>
</div>
) : (
<div className = { styles.labelDescription }>
{t(`${_TNS}.unsupportedBrowser`)}
</div>
)}
<div className = { styles.separator } />
<DialInSummary
className = 'deep-linking-dial-in'
clickableNumbers = { true }
hideError = { true }
room = { room }
url = { url } />
</div>
</div>
);
};
export default translate(DeepLinkingMobilePage);

View File

@@ -0,0 +1 @@
export default {};

View File

@@ -0,0 +1,77 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createDeepLinkingPageEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState } from '../../app/types';
import { IDeeplinkingConfig } from '../../base/config/configType';
/**
* The type of the React {@code Component} props of
* {@link NoMobileApp}.
*/
interface IProps {
/**
* The deeplinking config.
*/
_deeplinkingCfg: IDeeplinkingConfig;
}
/**
* React component representing no mobile app page.
*
* @class NoMobileApp
*/
class NoMobileApp extends Component<IProps> {
/**
* Implements the Component's componentDidMount method.
*
* @inheritdoc
*/
override componentDidMount() {
sendAnalytics(
createDeepLinkingPageEvent(
'displayed', 'noMobileApp', { isMobileBrowser: true }));
}
/**
* Renders the component.
*
* @returns {ReactElement}
*/
override render() {
const ns = 'no-mobile-app';
const { desktop } = this.props._deeplinkingCfg;
const { appName } = desktop ?? {};
return (
<div className = { ns }>
<h2 className = { `${ns}__title` }>
Video chat isn't available on mobile.
</h2>
<p className = { `${ns}__description` }>
Please use { appName } on desktop to
join calls.
</p>
</div>
);
}
}
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code NoMobileApp} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
return {
_deeplinkingCfg: state['features/base/config'].deeplinking || {}
};
}
export default connect(_mapStateToProps)(NoMobileApp);

View File

@@ -0,0 +1,6 @@
/**
* The namespace of the i18n/translation keys.
*
* @type {string}
*/
export const _TNS = 'deepLinking';

View File

@@ -0,0 +1,96 @@
import { IReduxState } from '../app/types';
import { isMobileBrowser } from '../base/environment/utils';
import { browser } from '../base/lib-jitsi-meet';
import Platform from '../base/react/Platform';
import { URI_PROTOCOL_PATTERN } from '../base/util/uri';
import { isVpaasMeeting } from '../jaas/functions';
import DeepLinkingDesktopPage from './components/DeepLinkingDesktopPage';
import DeepLinkingMobilePage from './components/DeepLinkingMobilePage';
import NoMobileApp from './components/NoMobileApp';
import { _openDesktopApp } from './openDesktopApp.web';
/**
* Generates a deep linking URL based on the current window URL.
*
* @param {Object} state - Object containing current redux state.
*
* @returns {string} - The generated URL.
*/
export function generateDeepLinkingURL(state: IReduxState) {
// If the user installed the app while this Component was displayed
// (e.g. the user clicked the Download the App button), then we would
// like to open the current URL in the mobile app. The only way to do it
// appears to be a link with an app-specific scheme, not a Universal
// Link.
const { href } = window.location;
const regex = new RegExp(URI_PROTOCOL_PATTERN, 'gi');
// @ts-ignore
const mobileConfig = state['features/base/config'].deeplinking?.[Platform.OS] || {};
const { appScheme, appPackage } = mobileConfig;
// Android: use an intent link, custom schemes don't work in all browsers.
// https://developer.chrome.com/multidevice/android/intents
if (Platform.OS === 'android') {
// https://meet.jit.si/foo -> meet.jit.si/foo
const url = href.replace(regex, '').substr(2);
return `intent://${url}#Intent;scheme=${appScheme};package=${appPackage};end`;
}
// iOS: Replace the protocol part with the app scheme.
return href.replace(regex, `${appScheme}:`);
}
/**
* Resolves with the component that should be displayed if the deep linking page
* should be shown and with <tt>undefined</tt> otherwise.
*
* @param {Object} state - Object containing current redux state.
* @returns {Promise<Component>}
*/
export function getDeepLinkingPage(state: IReduxState) {
const { room } = state['features/base/conference'];
const { launchInWeb } = state['features/deep-linking'];
const deeplinking = state['features/base/config'].deeplinking || {};
// @ts-ignore
const { appScheme } = deeplinking?.[Platform.OS as keyof typeof deeplinking] || {};
// Show only if we are about to join a conference.
if (launchInWeb
|| !room
|| state['features/base/config'].deeplinking?.disabled
|| browser.isElectron()
|| (isVpaasMeeting(state) && (!appScheme || appScheme === 'com.8x8.meet'))) {
return Promise.resolve();
}
if (isMobileBrowser()) { // mobile
const mobileAppPromo
= typeof interfaceConfig === 'object'
&& interfaceConfig.MOBILE_APP_PROMO;
return Promise.resolve(
typeof mobileAppPromo === 'undefined' || Boolean(mobileAppPromo)
? DeepLinkingMobilePage : NoMobileApp);
}
return _openDesktopApp(state).then(
// eslint-disable-next-line no-confusing-arrow
result => result ? DeepLinkingDesktopPage : undefined);
}
/**
* Opens the desktop app.
*
* @param {Object} state - Object containing current redux state.
* @returns {Promise<boolean>} - Resolves with true if the attempt to open the desktop app was successful and resolves
* with false otherwise.
*/
export function openDesktopApp(state: IReduxState) {
return _openDesktopApp(state);
}

View File

@@ -0,0 +1,20 @@
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { OPEN_DESKTOP_APP } from './actionTypes';
import { openDesktopApp } from './functions.web';
/**
* Implements the middleware of the deep linking feature.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case OPEN_DESKTOP_APP:
openDesktopApp(store.getState());
break;
}
return next(action);
});

View File

@@ -0,0 +1,29 @@
import { executeAfterLoad } from '../app/functions.web';
import { IReduxState } from '../app/types';
import { URI_PROTOCOL_PATTERN } from '../base/util/uri';
/**
* Opens the desktop app.
*
* @param {Object} _state - Object containing current redux state.
* @returns {Promise<boolean>} - Resolves with true if the attempt to open the desktop app was successful and resolves
* with false otherwise.
*/
export function _openDesktopApp(_state: Object) {
const state = _state as IReduxState;
const deeplinkingDesktop = state['features/base/config'].deeplinking?.desktop;
if (deeplinkingDesktop?.enabled) {
const { appScheme } = deeplinkingDesktop;
const regex = new RegExp(URI_PROTOCOL_PATTERN, 'gi');
// This is needed to workaround https://issues.chromium.org/issues/41398687
executeAfterLoad(() => {
window.location.href = window.location.href.replace(regex, `${appScheme}:`);
});
return Promise.resolve(true);
}
return Promise.resolve(false);
}

View File

@@ -0,0 +1,20 @@
import ReducerRegistry from '../base/redux/ReducerRegistry';
import { OPEN_WEB_APP } from './actionTypes';
export interface IDeepLinkingState {
launchInWeb?: boolean;
}
ReducerRegistry.register<IDeepLinkingState>('features/deep-linking', (state = {}, action): IDeepLinkingState => {
switch (action.type) {
case OPEN_WEB_APP: {
return {
...state,
launchInWeb: true
};
}
}
return state;
});