This commit is contained in:
18
react/features/deep-linking/actionTypes.ts
Normal file
18
react/features/deep-linking/actionTypes.ts
Normal 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';
|
||||
33
react/features/deep-linking/actions.ts
Normal file
33
react/features/deep-linking/actions.ts
Normal 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
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default {};
|
||||
@@ -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`)
|
||||
}
|
||||
<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);
|
||||
@@ -0,0 +1 @@
|
||||
export default {};
|
||||
@@ -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);
|
||||
@@ -0,0 +1 @@
|
||||
export default {};
|
||||
77
react/features/deep-linking/components/NoMobileApp.web.tsx
Normal file
77
react/features/deep-linking/components/NoMobileApp.web.tsx
Normal 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);
|
||||
6
react/features/deep-linking/constants.ts
Normal file
6
react/features/deep-linking/constants.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* The namespace of the i18n/translation keys.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const _TNS = 'deepLinking';
|
||||
96
react/features/deep-linking/functions.web.ts
Normal file
96
react/features/deep-linking/functions.web.ts
Normal 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);
|
||||
}
|
||||
20
react/features/deep-linking/middleware.web.ts
Normal file
20
react/features/deep-linking/middleware.web.ts
Normal 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);
|
||||
});
|
||||
29
react/features/deep-linking/openDesktopApp.web.ts
Normal file
29
react/features/deep-linking/openDesktopApp.web.ts
Normal 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);
|
||||
}
|
||||
20
react/features/deep-linking/reducer.ts
Normal file
20
react/features/deep-linking/reducer.ts
Normal 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;
|
||||
});
|
||||
Reference in New Issue
Block a user