feat(deeplinking) Refactor deeplinking (#12950)
- redesign deeplinking mobile page, desktop page and dial in number page - now dial in number page is an entry point in app.bundle.
This commit is contained in:
parent
9b7a5ffdd1
commit
35ee92869f
2
Makefile
2
Makefile
|
@ -48,8 +48,6 @@ deploy-appbundle:
|
|||
$(BUILD_DIR)/do_external_connect.min.js.map \
|
||||
$(BUILD_DIR)/external_api.min.js \
|
||||
$(BUILD_DIR)/external_api.min.js.map \
|
||||
$(BUILD_DIR)/dial_in_info_bundle.min.js \
|
||||
$(BUILD_DIR)/dial_in_info_bundle.min.js.map \
|
||||
$(BUILD_DIR)/alwaysontop.min.js \
|
||||
$(BUILD_DIR)/alwaysontop.min.js.map \
|
||||
$(OUTPUT_DIR)/analytics-ga.js \
|
||||
|
|
|
@ -1146,6 +1146,13 @@ var config = {
|
|||
// }
|
||||
// },
|
||||
|
||||
// // The terms, privacy and help centre URL's.
|
||||
// legalUrls: {
|
||||
// helpCentre: 'https://web-cdn.jitsi.net/faq/meet-faq.html',
|
||||
// privacy: 'https://jitsi.org/meet/privacy',
|
||||
// terms: 'https://jitsi.org/meet/terms'
|
||||
// },
|
||||
|
||||
// A property to disable the right click context menu for localVideo
|
||||
// the menu has option to flip the locally seen video for local presentations
|
||||
// disableLocalVideoFlip: false,
|
||||
|
|
|
@ -201,11 +201,6 @@ $deepLinkingDialInConferenceIdPadding: inherit;
|
|||
$deepLinkingDialInConferenceIdBackgroundColor: inherit;
|
||||
$deepLinkingDialInConferenceIdBorderRadius: inherit;
|
||||
|
||||
$deepLinkingDialInConferenceNameFontSize: inherit;
|
||||
$deepLinkingDialInConferenceNameLineHeight: inherit;
|
||||
$deepLinkingDialInConferenceNameMarginBottom: none;
|
||||
$deepLinkingDialInConferenceNameFontWeight: inherit;
|
||||
|
||||
$deepLinkingDialInConferenceDescriptionFontSize: 0.8em;
|
||||
$deepLinkingDialInConferenceDescriptionLineHeight: inherit;
|
||||
$deepLinkingDialInConferenceDescriptionMarginBottom: none;
|
||||
|
|
|
@ -67,6 +67,13 @@
|
|||
font-size: 1em;
|
||||
}
|
||||
|
||||
|
||||
.dial-in-conference-id {
|
||||
text-align: center;
|
||||
min-width: 200px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.dial-in-conference-id {
|
||||
margin: $deepLinkingDialInConferenceIdMargin;
|
||||
padding: $deepLinkingDialInConferenceIdPadding;
|
||||
|
@ -74,24 +81,12 @@
|
|||
border-radius: $deepLinkingDialInConferenceIdBorderRadius;
|
||||
}
|
||||
|
||||
.dial-in-conference-name {
|
||||
font-size: $deepLinkingDialInConferenceNameFontSize;
|
||||
line-height: $deepLinkingDialInConferenceNameLineHeight;
|
||||
margin-bottom: $deepLinkingDialInConferenceNameMarginBottom;
|
||||
font-weight: $deepLinkingDialInConferenceNameFontWeight;
|
||||
}
|
||||
|
||||
.dial-in-conference-description {
|
||||
font-size: $deepLinkingDialInConferenceDescriptionFontSize;
|
||||
line-height: $deepLinkingDialInConferenceDescriptionLineHeight;
|
||||
margin-bottom: $deepLinkingDialInConferenceDescriptionMarginBottom;
|
||||
}
|
||||
|
||||
.dial-in-conference-pin {
|
||||
font-size: $deepLinkingDialInConferencePinFontSize;
|
||||
line-height: $deepLinkingDialInConferencePinLineHeight;
|
||||
}
|
||||
|
||||
.toll-free-list {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
|
|
@ -50,6 +50,8 @@
|
|||
}
|
||||
|
||||
.dial-in-numbers-list {
|
||||
max-width: 334px;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
|
@ -59,10 +61,6 @@
|
|||
text-align: left;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid #d1dbe8;
|
||||
}
|
||||
|
||||
.flag-cell {
|
||||
vertical-align: top;
|
||||
width: 30px;
|
||||
|
@ -91,6 +89,7 @@
|
|||
font-weight: bold;
|
||||
list-style: none;
|
||||
vertical-align: top;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
li.toll-free:empty:before {
|
||||
|
@ -119,11 +118,6 @@
|
|||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.dial-in-conference-name,
|
||||
.dial-in-conference-pin {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.dial-in-conference-description {
|
||||
margin: 12px;
|
||||
}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 6.3 KiB |
|
@ -184,13 +184,21 @@
|
|||
"deepLinking": {
|
||||
"appNotInstalled": "You need the {{app}} mobile app to join this meeting on your phone.",
|
||||
"description": "Nothing happened? We tried launching your meeting in the {{app}} desktop app. Try again or launch it in the {{app}} web app.",
|
||||
"descriptionNew": "Nothing happened? We tried launching your meeting in the {{app}} desktop app. <br /><br /> You can try again or launch it on web.",
|
||||
"descriptionWithoutWeb": "Nothing happened? We tried launching your meeting in the {{app}} desktop app.",
|
||||
"downloadApp": "Download the app",
|
||||
"downloadMobileApp": "Download from App Store",
|
||||
"ifDoNotHaveApp": "If you don't have the app yet:",
|
||||
"ifHaveApp": "If you already have the app:",
|
||||
"joinInApp": "Join this meeting using the app",
|
||||
"joinInAppNew": "Join in app",
|
||||
"joinInBrowser": "Join in browser",
|
||||
"launchMeetingLabel": "How do you want to join this meeting?",
|
||||
"launchWebButton": "Launch in web",
|
||||
"noMobileApp": "You don’t have the app?",
|
||||
"termsAndConditions": "By continuing you agree to our <a href='{{termsAndConditionsLink}}' rel='noopener noreferrer' target='_blank'>terms & conditions.</a>",
|
||||
"title": "Launching your meeting in {{app}}...",
|
||||
"titleNew": "Launching your meeting ...",
|
||||
"tryAgainButton": "Try again in desktop",
|
||||
"unsupportedBrowser": "It looks like you're using a browser we don't support."
|
||||
},
|
||||
|
|
|
@ -388,6 +388,11 @@ export interface IConfig {
|
|||
lastNLimits?: {
|
||||
[key: number]: number;
|
||||
};
|
||||
legalUrls?: {
|
||||
helpCentre: string;
|
||||
privacy: string;
|
||||
terms: string;
|
||||
};
|
||||
liveStreaming?: {
|
||||
dataPrivacyLink?: string;
|
||||
enabled?: boolean;
|
||||
|
|
|
@ -70,3 +70,18 @@ export const THIRD_PARTY_PREJOIN_BUTTONS = [ 'microphone', 'camera', 'select-bac
|
|||
export const FEATURE_FLAGS = {
|
||||
SSRC_REWRITING: 'ssrcRewritingEnabled'
|
||||
};
|
||||
|
||||
/**
|
||||
* The URL at which the terms (of service/use) are available to the user.
|
||||
*/
|
||||
export const DEFAULT_TERMS_URL = 'https://jitsi.org/meet/terms';
|
||||
|
||||
/**
|
||||
* The URL at which the privacy policy is available to the user.
|
||||
*/
|
||||
export const DEFAULT_PRIVACY_URL = 'https://jitsi.org/meet/privacy';
|
||||
|
||||
/**
|
||||
* The URL at which the help centre is available to the user.
|
||||
*/
|
||||
export const DEFAULT_HELP_CENTRE_URL = 'https://web-cdn.jitsi.net/faq/meet-faq.html';
|
||||
|
|
|
@ -11,7 +11,13 @@ import { parseURLParams } from '../util/parseURLParams';
|
|||
|
||||
import { IConfig } from './configType';
|
||||
import CONFIG_WHITELIST from './configWhitelist';
|
||||
import { FEATURE_FLAGS, _CONFIG_STORE_PREFIX } from './constants';
|
||||
import {
|
||||
DEFAULT_HELP_CENTRE_URL,
|
||||
DEFAULT_PRIVACY_URL,
|
||||
DEFAULT_TERMS_URL,
|
||||
FEATURE_FLAGS,
|
||||
_CONFIG_STORE_PREFIX
|
||||
} from './constants';
|
||||
import INTERFACE_CONFIG_WHITELIST from './interfaceConfigWhitelist';
|
||||
import logger from './logger';
|
||||
|
||||
|
@ -326,3 +332,24 @@ export function getDialOutUrl(state: IReduxState) {
|
|||
export function getSecurityUiConfig(state: IReduxState) {
|
||||
return state['features/base/config']?.securityUi || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the terms, privacy and help centre URL's.
|
||||
*
|
||||
* @param {IReduxState} state - The state of the application.
|
||||
* @returns {{
|
||||
* privacy: string,
|
||||
* helpCentre: string,
|
||||
* terms: string
|
||||
* }}
|
||||
*/
|
||||
export function getLegalUrls(state: IReduxState) {
|
||||
const helpCentreURL = state['features/base/config']?.helpCentreURL;
|
||||
const configLegalUrls = state['features/base/config']?.legalUrls;
|
||||
|
||||
return {
|
||||
privacy: configLegalUrls?.privacy || DEFAULT_PRIVACY_URL,
|
||||
helpCentre: helpCentreURL || configLegalUrls?.helpCentre || DEFAULT_HELP_CENTRE_URL,
|
||||
terms: configLegalUrls?.terms || DEFAULT_TERMS_URL
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// @flow
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { appNavigate } from '../app/actions';
|
|
@ -1,192 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||
import React, { Component } from 'react';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { createDeepLinkingPageEvent, sendAnalytics } from '../../analytics';
|
||||
import { IDeeplinkingConfig } from '../../base/config/configType';
|
||||
import { isSupportedBrowser } from '../../base/environment';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { connect } from '../../base/redux';
|
||||
import Button from '../../base/ui/components/web/Button';
|
||||
import { BUTTON_TYPES } from '../../base/ui/constants.web';
|
||||
import {
|
||||
openDesktopApp,
|
||||
openWebApp
|
||||
} from '../actions';
|
||||
import { _TNS } from '../constants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link DeepLinkingDesktopPage}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The deeplinking config.
|
||||
*/
|
||||
_deeplinkingCfg: IDeeplinkingConfig,
|
||||
|
||||
/**
|
||||
* Used to dispatch actions from the buttons.
|
||||
*/
|
||||
dispatch: Dispatch<any>,
|
||||
|
||||
/**
|
||||
* Used to obtain translations.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* React component representing the deep linking page.
|
||||
*
|
||||
* @class DeepLinkingDesktopPage
|
||||
*/
|
||||
class DeepLinkingDesktopPage<P : Props> extends Component<P> {
|
||||
/**
|
||||
* Initializes a new {@code DeepLinkingDesktopPage} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onLaunchWeb = this._onLaunchWeb.bind(this);
|
||||
this._onTryAgain = this._onTryAgain.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the Component's componentDidMount method.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'displayed', 'DeepLinkingDesktop', { isMobileBrowser: false }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the component.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t, _deeplinkingCfg: { desktop = {}, hideLogo, showImage } } = this.props;
|
||||
const { appName } = desktop;
|
||||
const rightColumnStyle
|
||||
= showImage ? null : { width: '100%' };
|
||||
|
||||
return (
|
||||
|
||||
// Enabling light theme because of the color of the buttons.
|
||||
<AtlasKitThemeProvider mode = 'light'>
|
||||
<div className = 'deep-linking-desktop'>
|
||||
<div className = 'header'>
|
||||
{
|
||||
hideLogo
|
||||
? null
|
||||
: <img
|
||||
alt = { t('welcomepage.logo.logoDeepLinking') }
|
||||
className = 'logo'
|
||||
src = 'images/logo-deep-linking.png' />
|
||||
}
|
||||
</div>
|
||||
<div className = 'content'>
|
||||
{
|
||||
showImage
|
||||
? <div className = 'leftColumn'>
|
||||
<div className = 'leftColumnContent'>
|
||||
<div className = 'image' />
|
||||
</div>
|
||||
</div> : null
|
||||
}
|
||||
<div
|
||||
className = 'rightColumn'
|
||||
style = { rightColumnStyle }>
|
||||
<div className = 'rightColumnContent'>
|
||||
<h1 className = 'title'>
|
||||
{
|
||||
t(`${_TNS}.title`,
|
||||
{ app: appName })
|
||||
}
|
||||
</h1>
|
||||
<p className = 'description'>
|
||||
{
|
||||
t(
|
||||
`${_TNS}.${isSupportedBrowser()
|
||||
? 'description'
|
||||
: 'descriptionWithoutWeb'}`,
|
||||
{ app: appName }
|
||||
)
|
||||
}
|
||||
</p>
|
||||
<div className = 'buttons'>
|
||||
<Button
|
||||
label = { t(`${_TNS}.tryAgainButton`) }
|
||||
onClick = { this._onTryAgain }
|
||||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
{
|
||||
isSupportedBrowser()
|
||||
&& <Button
|
||||
label = { t(`${_TNS}.launchWebButton`) }
|
||||
onClick = { this._onLaunchWeb }
|
||||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AtlasKitThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
_onTryAgain: () => void;
|
||||
|
||||
/**
|
||||
* Handles try again button clicks.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onTryAgain() {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'clicked', 'tryAgainButton', { isMobileBrowser: false }));
|
||||
this.props.dispatch(openDesktopApp());
|
||||
}
|
||||
|
||||
_onLaunchWeb: () => void;
|
||||
|
||||
/**
|
||||
* Handles launch web button clicks.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onLaunchWeb() {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'clicked', 'launchWebButton', { isMobileBrowser: false }));
|
||||
this.props.dispatch(openWebApp());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code DeepLinkingDesktopPage} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_deeplinkingCfg: state['features/base/config'].deeplinking || {}
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(DeepLinkingDesktopPage));
|
|
@ -0,0 +1,159 @@
|
|||
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 { withPixelLineHeight } from '../../base/styles/functions.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,
|
||||
...withPixelLineHeight(theme.typography.heading4)
|
||||
},
|
||||
roomName: {
|
||||
marginBottom: 32,
|
||||
...withPixelLineHeight(theme.typography.heading5)
|
||||
},
|
||||
descriptionLabel: {
|
||||
marginBottom: 32,
|
||||
...withPixelLineHeight(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,
|
||||
...withPixelLineHeight(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 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.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);
|
|
@ -1,299 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { createDeepLinkingPageEvent, sendAnalytics } from '../../analytics';
|
||||
import { IDeeplinkingConfig, IDeeplinkingMobileConfig } from '../../base/config/configType';
|
||||
import { isSupportedMobileBrowser } from '../../base/environment';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { Platform } from '../../base/react';
|
||||
import { connect } from '../../base/redux';
|
||||
import { DialInSummary } from '../../invite';
|
||||
import { openWebApp } from '../actions';
|
||||
import { _TNS } from '../constants';
|
||||
import { generateDeepLinkingURL } from '../functions';
|
||||
import { renderPromotionalFooter } from '../renderPromotionalFooter';
|
||||
|
||||
/**
|
||||
* The namespace of the CSS styles of DeepLinkingMobilePage.
|
||||
*
|
||||
* @private
|
||||
* @type {string}
|
||||
*/
|
||||
const _SNS = 'deep-linking-mobile';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link DeepLinkingMobilePage}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The deeplinking config.
|
||||
*/
|
||||
_deeplinkingCfg: IDeeplinkingConfig,
|
||||
|
||||
/**
|
||||
* Application mobile deeplinking config.
|
||||
*/
|
||||
_mobileConfig: IDeeplinkingMobileConfig,
|
||||
|
||||
/**
|
||||
* The deeplinking url.
|
||||
*/
|
||||
_deepLinkingUrl: string,
|
||||
|
||||
/**
|
||||
* The name of the conference attempting to being joined.
|
||||
*/
|
||||
_room: string,
|
||||
|
||||
/**
|
||||
* The page current url.
|
||||
*/
|
||||
_url: URL,
|
||||
|
||||
/**
|
||||
* Used to dispatch actions from the buttons.
|
||||
*/
|
||||
dispatch: Dispatch<any>,
|
||||
|
||||
/**
|
||||
* The function to translate human-readable text.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* React component representing mobile browser page.
|
||||
*
|
||||
* @class DeepLinkingMobilePage
|
||||
*/
|
||||
class DeepLinkingMobilePage extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code DeepLinkingMobilePage} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onDownloadApp = this._onDownloadApp.bind(this);
|
||||
this._onLaunchWeb = this._onLaunchWeb.bind(this);
|
||||
this._onOpenApp = this._onOpenApp.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the Component's componentDidMount method.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'displayed', 'DeepLinkingMobile', { isMobileBrowser: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_deeplinkingCfg: { hideLogo },
|
||||
_mobileConfig: { downloadLink, appName },
|
||||
_room,
|
||||
t,
|
||||
_url,
|
||||
_deepLinkingUrl
|
||||
} = this.props;
|
||||
const downloadButtonClassName
|
||||
= `${_SNS}__button ${_SNS}__button_primary`;
|
||||
|
||||
|
||||
const onOpenLinkProperties = downloadLink
|
||||
? {
|
||||
// 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.
|
||||
}
|
||||
: {
|
||||
// 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'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className = { _SNS }>
|
||||
<div className = 'header'>
|
||||
{
|
||||
hideLogo
|
||||
? null
|
||||
: <img
|
||||
alt = { t('welcomepage.logo.logoDeepLinking') }
|
||||
className = 'logo'
|
||||
src = 'images/logo-deep-linking.png' />
|
||||
}
|
||||
</div>
|
||||
<div className = { `${_SNS}__body` }>
|
||||
<p className = { `${_SNS}__text` }>
|
||||
{ t(`${_TNS}.appNotInstalled`, { app: appName }) }
|
||||
</p>
|
||||
<p className = { `${_SNS}__text` }>
|
||||
{ t(`${_TNS}.ifHaveApp`) }
|
||||
</p>
|
||||
<a
|
||||
{ ...onOpenLinkProperties }
|
||||
className = { `${_SNS}__href` }
|
||||
href = { _deepLinkingUrl }
|
||||
onClick = { this._onOpenApp }
|
||||
target = '_top'>
|
||||
<button className = { `${_SNS}__button ${_SNS}__button_primary` }>
|
||||
{ t(`${_TNS}.joinInApp`) }
|
||||
</button>
|
||||
</a>
|
||||
<p className = { `${_SNS}__text` }>
|
||||
{ t(`${_TNS}.ifDoNotHaveApp`) }
|
||||
</p>
|
||||
<a
|
||||
{ ...onOpenLinkProperties }
|
||||
href = { this._generateDownloadURL() }
|
||||
onClick = { this._onDownloadApp }
|
||||
target = '_top'>
|
||||
<button className = { downloadButtonClassName }>
|
||||
{ t(`${_TNS}.downloadApp`) }
|
||||
</button>
|
||||
</a>
|
||||
{
|
||||
isSupportedMobileBrowser()
|
||||
? (
|
||||
<a
|
||||
onClick = { this._onLaunchWeb }
|
||||
target = '_top'>
|
||||
<button className = { downloadButtonClassName }>
|
||||
{ t(`${_TNS}.launchWebButton`) }
|
||||
</button>
|
||||
</a>
|
||||
) : (
|
||||
<b>
|
||||
{ t(`${_TNS}.unsupportedBrowser`) }
|
||||
</b>
|
||||
)
|
||||
}
|
||||
{ renderPromotionalFooter() }
|
||||
<DialInSummary
|
||||
className = 'deep-linking-dial-in'
|
||||
clickableNumbers = { true }
|
||||
room = { _room }
|
||||
url = { _url } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the URL for downloading the app.
|
||||
*
|
||||
* @private
|
||||
* @returns {string} - The URL for downloading the app.
|
||||
*/
|
||||
_generateDownloadURL() {
|
||||
const { _mobileConfig: { downloadLink, dynamicLink, appScheme } } = this.props;
|
||||
|
||||
if (downloadLink && typeof dynamicLink === 'undefined') {
|
||||
return downloadLink;
|
||||
}
|
||||
|
||||
const {
|
||||
apn,
|
||||
appCode,
|
||||
customDomain,
|
||||
ibi,
|
||||
isi
|
||||
} = dynamicLink || {};
|
||||
|
||||
const domain = customDomain ?? `https://${appCode}.app.goo.gl`;
|
||||
|
||||
return `${domain}/?link=${
|
||||
encodeURIComponent(window.location.href)}&apn=${
|
||||
apn}&ibi=${
|
||||
ibi}&isi=${
|
||||
isi}&ius=${
|
||||
appScheme}&efr=1`;
|
||||
}
|
||||
|
||||
_onDownloadApp: () => void;
|
||||
|
||||
/**
|
||||
* Handles download app button clicks.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDownloadApp() {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'clicked', 'downloadAppButton', { isMobileBrowser: true }));
|
||||
}
|
||||
|
||||
_onLaunchWeb: () => void;
|
||||
|
||||
/**
|
||||
* Handles launch web button clicks.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onLaunchWeb() {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'clicked', 'launchWebButton', { isMobileBrowser: true }));
|
||||
this.props.dispatch(openWebApp());
|
||||
}
|
||||
|
||||
_onOpenApp: () => void;
|
||||
|
||||
/**
|
||||
* Handles open app button clicks.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onOpenApp() {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'clicked', 'openAppButton', { isMobileBrowser: true }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code DeepLinkingMobilePage} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { locationURL = {} } = state['features/base/connection'];
|
||||
const { deeplinking } = state['features/base/config'];
|
||||
const mobileConfig = deeplinking?.[Platform.OS] || {};
|
||||
|
||||
return {
|
||||
_deeplinkingCfg: deeplinking || {},
|
||||
_mobileConfig: mobileConfig,
|
||||
_room: decodeURIComponent(state['features/base/conference'].room),
|
||||
_url: locationURL,
|
||||
_deepLinkingUrl: generateDeepLinkingURL(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(DeepLinkingMobilePage));
|
|
@ -0,0 +1,241 @@
|
|||
/* 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 { withPixelLineHeight } from '../../base/styles/functions.web';
|
||||
import Button from '../../base/ui/components/web/Button';
|
||||
// @ts-ignore
|
||||
import DialInSummary from '../../invite/components/dial-in-summary/web/DialInSummary';
|
||||
import { openWebApp } from '../actions';
|
||||
// @ts-ignore
|
||||
import { _TNS } from '../constants';
|
||||
// @ts-ignore
|
||||
import { generateDeepLinkingURL } from '../functions';
|
||||
|
||||
|
||||
const PADDINGS = {
|
||||
topBottom: 24,
|
||||
leftRight: 40
|
||||
};
|
||||
|
||||
const useStyles = makeStyles()((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
background: '#1E1E1E',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
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,
|
||||
...withPixelLineHeight(theme.typography.heading5)
|
||||
},
|
||||
roomNameLabel: {
|
||||
...withPixelLineHeight(theme.typography.bodyLongRegularLarge)
|
||||
},
|
||||
joinMeetWrapper: {
|
||||
marginTop: 24,
|
||||
width: '100%'
|
||||
},
|
||||
labelDescription: {
|
||||
textAlign: 'center',
|
||||
marginTop: 16,
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegularLarge)
|
||||
},
|
||||
linkWrapper: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 8,
|
||||
width: '100%'
|
||||
},
|
||||
linkLabel: {
|
||||
color: theme.palette.link01,
|
||||
...withPixelLineHeight(theme.typography.bodyLongBoldLarge)
|
||||
},
|
||||
supportedBrowserContent: {
|
||||
marginTop: 16,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
labelOr: {
|
||||
...withPixelLineHeight(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, dynamicLink, appScheme }
|
||||
= (deeplinkingCfg?.[Platform.OS as keyof typeof deeplinkingCfg] || {}) as IDeeplinkingMobileConfig;
|
||||
|
||||
if (downloadLink && typeof dynamicLink === 'undefined') {
|
||||
return downloadLink;
|
||||
}
|
||||
|
||||
const {
|
||||
apn,
|
||||
appCode,
|
||||
customDomain,
|
||||
ibi,
|
||||
isi
|
||||
} = dynamicLink || {};
|
||||
|
||||
const domain = customDomain ?? `https://${appCode}.app.goo.gl`;
|
||||
|
||||
return `${domain}/?link=${
|
||||
encodeURIComponent(window.location.href)}&apn=${
|
||||
apn}&ibi=${
|
||||
ibi}&isi=${
|
||||
isi}&ius=${
|
||||
appScheme}&efr=1`;
|
||||
}, [ 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 }>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 }
|
||||
room = { room }
|
||||
url = { url } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default translate(DeepLinkingMobilePage);
|
|
@ -1,9 +0,0 @@
|
|||
// @flow
|
||||
/**
|
||||
* Method used in order to render a custom promotional footer.
|
||||
*
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export function renderPromotionalFooter() {
|
||||
return null;
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { _formatConferenceIDPin } from '../../../_utils';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ConferenceID}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The conference ID for dialing in.
|
||||
*/
|
||||
conferenceID: number,
|
||||
|
||||
/**
|
||||
* The name of the conference.
|
||||
*/
|
||||
conferenceName: ?string,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays a conference ID used as a pin for dialing into a conference.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class ConferenceID extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { conferenceID, conferenceName, t } = this.props;
|
||||
|
||||
return (
|
||||
<div className = 'dial-in-conference-id'>
|
||||
<div className = 'dial-in-conference-name'>
|
||||
{ conferenceName }
|
||||
</div>
|
||||
<div className = 'dial-in-conference-description'>
|
||||
{ t('info.dialANumber') }
|
||||
</div>
|
||||
<div className = 'dial-in-conference-pin'>
|
||||
{ `${t('info.dialInConferenceID')} ${_formatConferenceIDPin(conferenceID)}` }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(ConferenceID);
|
|
@ -0,0 +1,77 @@
|
|||
/* eslint-disable lines-around-comment */
|
||||
import { Theme } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import { withPixelLineHeight } from '../../../../base/styles/functions.web';
|
||||
// @ts-ignore
|
||||
import { _formatConferenceIDPin } from '../../../_utils';
|
||||
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The conference id.
|
||||
*/
|
||||
conferenceID?: string | number;
|
||||
|
||||
/**
|
||||
* The conference name.
|
||||
*/
|
||||
conferenceName: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
marginTop: 32,
|
||||
maxWidth: 310,
|
||||
padding: '16px 12px',
|
||||
background: theme.palette.ui02,
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: 6
|
||||
},
|
||||
confNameLabel: {
|
||||
...withPixelLineHeight(theme.typography.heading6),
|
||||
marginBottom: 18,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
},
|
||||
descriptionLabel: {
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegularLarge),
|
||||
marginBottom: 18
|
||||
},
|
||||
separator: {
|
||||
width: '100%',
|
||||
height: 1,
|
||||
background: theme.palette.ui04,
|
||||
marginBottom: 18
|
||||
},
|
||||
pinLabel: {
|
||||
...withPixelLineHeight(theme.typography.heading6)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const ConferenceID: React.FC<IProps> = ({ conferenceID, t }) => {
|
||||
const { classes: styles } = useStyles();
|
||||
|
||||
return (
|
||||
<div className = { styles.container }>
|
||||
<div className = { styles.descriptionLabel }>
|
||||
To join the meeting via phone, dial one of these numbers and then enter the pin
|
||||
</div>
|
||||
<div className = { styles.separator } />
|
||||
<div className = { styles.pinLabel }>
|
||||
{ `${t('info.dialInConferenceID')} ${_formatConferenceIDPin(conferenceID ?? '')}` }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default translate(ConferenceID);
|
|
@ -1,8 +1,13 @@
|
|||
// @flow
|
||||
|
||||
|
||||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import clsx from 'clsx';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { withPixelLineHeight } from '../../../../base/styles/functions.web';
|
||||
import { getDialInConferenceID, getDialInNumbers } from '../../../_utils';
|
||||
|
||||
import ConferenceID from './ConferenceID';
|
||||
|
@ -20,6 +25,11 @@ type Props = {
|
|||
*/
|
||||
className: string,
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
*/
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* Whether or not numbers should include links with the telephone protocol.
|
||||
*/
|
||||
|
@ -30,6 +40,16 @@ type Props = {
|
|||
*/
|
||||
room: string,
|
||||
|
||||
/**
|
||||
* Whether the dial in summary container is scrollable.
|
||||
*/
|
||||
scrollable: Boolean,
|
||||
|
||||
/**
|
||||
* Whether the room name should show as title.
|
||||
*/
|
||||
showTitle?: boolean,
|
||||
|
||||
/**
|
||||
* The url where we were loaded.
|
||||
*/
|
||||
|
@ -72,6 +92,26 @@ type State = {
|
|||
numbersEnabled: ?boolean
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
hasNumbers: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: '#1E1E1E',
|
||||
color: theme.palette.text01
|
||||
},
|
||||
scrollable: {
|
||||
height: '100vh',
|
||||
overflowY: 'scroll'
|
||||
},
|
||||
roomName: {
|
||||
margin: '40px auto 8px',
|
||||
...withPixelLineHeight(theme.typography.heading5)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays a page listing numbers for dialing into a conference and pin to
|
||||
* the a specific conference.
|
||||
|
@ -136,24 +176,27 @@ class DialInSummary extends Component<Props, State> {
|
|||
let contents;
|
||||
|
||||
const { conferenceID, error, loading, numbersEnabled } = this.state;
|
||||
const { classes, showTitle, room, clickableNumbers, scrollable, t } = this.props;
|
||||
|
||||
if (loading) {
|
||||
contents = '';
|
||||
} else if (numbersEnabled === false) {
|
||||
contents = this.props.t('info.dialInNotSupported');
|
||||
contents = t('info.dialInNotSupported');
|
||||
} else if (error) {
|
||||
contents = error;
|
||||
} else {
|
||||
className = 'has-numbers';
|
||||
className = clsx(classes.hasNumbers, scrollable && classes.scrollable);
|
||||
contents = [
|
||||
conferenceID
|
||||
? <ConferenceID
|
||||
conferenceID = { conferenceID }
|
||||
conferenceName = { this.props.room }
|
||||
key = 'conferenceID' />
|
||||
: null,
|
||||
? <>
|
||||
{ showTitle && <div className = { classes.roomName }>{ room }</div> }
|
||||
<ConferenceID
|
||||
conferenceID = { conferenceID }
|
||||
conferenceName = { room }
|
||||
key = 'conferenceID' />
|
||||
</> : null,
|
||||
<NumbersList
|
||||
clickableNumbers = { this.props.clickableNumbers }
|
||||
clickableNumbers = { clickableNumbers }
|
||||
conferenceID = { conferenceID }
|
||||
key = 'numbers'
|
||||
numbers = { this.state.numbers } />
|
||||
|
@ -161,7 +204,7 @@ class DialInSummary extends Component<Props, State> {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className = { `${this.props.className} ${className}` }>
|
||||
<div className = { className }>
|
||||
{ contents }
|
||||
</div>
|
||||
);
|
||||
|
@ -272,4 +315,4 @@ class DialInSummary extends Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
export default translate(DialInSummary);
|
||||
export default translate(withStyles(styles)(DialInSummary));
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
// @flow
|
||||
|
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||
import React from 'react';
|
||||
|
||||
import { BaseApp } from '../../../../base/app';
|
||||
import { isMobileBrowser } from '../../../../base/environment/utils';
|
||||
import GlobalStyles from '../../../../base/ui/components/GlobalStyles.web';
|
||||
import JitsiThemeProvider from '../../../../base/ui/components/JitsiThemeProvider.web';
|
||||
import { parseURLParams } from '../../../../base/util';
|
||||
import { DIAL_IN_INFO_PAGE_PATH_NAME } from '../../../constants';
|
||||
import NoRoomError from '../../dial-in-info-page/NoRoomError.web';
|
||||
|
||||
import DialInSummary from './DialInSummary';
|
||||
|
||||
/**
|
||||
* Wrapper application for prejoin.
|
||||
*
|
||||
* @augments BaseApp
|
||||
*/
|
||||
export default class DialInSummaryApp extends BaseApp {
|
||||
/**
|
||||
* The deferred for the initialisation {{promise, resolve, reject}}.
|
||||
*/
|
||||
_init: Object;
|
||||
|
||||
/**
|
||||
* Navigates to {@link Prejoin} upon mount.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
async componentDidMount() {
|
||||
await super.componentDidMount();
|
||||
|
||||
const { room } = parseURLParams(window.location, true, 'search');
|
||||
const { href } = window.location;
|
||||
const ix = href.indexOf(DIAL_IN_INFO_PAGE_PATH_NAME);
|
||||
const url = (ix > 0 ? href.substring(0, ix) : href) + room;
|
||||
|
||||
super._navigate({
|
||||
component: () => (<>
|
||||
{room
|
||||
? <DialInSummary
|
||||
className = 'dial-in-page'
|
||||
clickableNumbers = { isMobileBrowser() }
|
||||
room = { decodeURIComponent(room) }
|
||||
scrollable = { true }
|
||||
showTitle = { true }
|
||||
url = { url } />
|
||||
: <NoRoomError className = 'dial-in-page' />}
|
||||
</>)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the parent method to inject {@link AtlasKitThemeProvider} as
|
||||
* the top most component.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_createMainElement(component, props) {
|
||||
return (
|
||||
<JitsiThemeProvider>
|
||||
<AtlasKitThemeProvider mode = 'dark'>
|
||||
<GlobalStyles />
|
||||
{super._createMainElement(component, props)}
|
||||
</AtlasKitThemeProvider>
|
||||
</JitsiThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the platform specific dialog container.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderDialogContainer() {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1,244 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { Icon, IconPhoneRinging } from '../../../../base/icons';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Whether or not numbers should include links with the telephone protocol.
|
||||
*/
|
||||
clickableNumbers: boolean,
|
||||
|
||||
/**
|
||||
* The conference ID for dialing in.
|
||||
*/
|
||||
conferenceID: number,
|
||||
|
||||
/**
|
||||
* The phone numbers to display. Can be an array of number Objects or an
|
||||
* object with countries as keys and an array of numbers as values.
|
||||
*/
|
||||
numbers: { [string]: Array<string> } | Array<Object>,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a table with phone numbers to dial in to a conference.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class NumbersList extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { numbers } = this.props;
|
||||
|
||||
return this._renderWithCountries(numbers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders rows of countries and associated phone numbers.
|
||||
*
|
||||
* @param {Object|Array<Object>} numbersMapping - An object with country
|
||||
* names as keys and values as arrays of phone numbers.
|
||||
* @private
|
||||
* @returns {ReactElement[]}
|
||||
*/
|
||||
_renderWithCountries(
|
||||
numbersMapping: { numbers: Array<string> } | Array<Object>) {
|
||||
const { t } = this.props;
|
||||
let hasFlags = false, numbers;
|
||||
|
||||
if (Array.isArray(numbersMapping)) {
|
||||
hasFlags = true;
|
||||
numbers = numbersMapping.reduce(
|
||||
(resultNumbers, number) => {
|
||||
// The i18n-iso-countries package insists on upper case.
|
||||
const countryCode = number.countryCode.toUpperCase();
|
||||
|
||||
let countryName;
|
||||
|
||||
if (countryCode === 'SIP') {
|
||||
countryName = t('info.sip');
|
||||
} else {
|
||||
countryName = t(`countries:countries.${countryCode}`);
|
||||
|
||||
// Some countries have multiple names as US ['United States of America', 'USA']
|
||||
// choose the first one if that is the case
|
||||
if (!countryName) {
|
||||
countryName = t(`countries:countries.${countryCode}.0`);
|
||||
}
|
||||
}
|
||||
|
||||
if (resultNumbers[countryName]) {
|
||||
resultNumbers[countryName].push(number);
|
||||
} else {
|
||||
resultNumbers[countryName] = [ number ];
|
||||
}
|
||||
|
||||
return resultNumbers;
|
||||
}, {});
|
||||
} else {
|
||||
numbers = {};
|
||||
|
||||
for (const [ country, numbersArray ]
|
||||
of Object.entries(numbersMapping.numbers)) {
|
||||
|
||||
if (Array.isArray(numbersArray)) {
|
||||
/* eslint-disable arrow-body-style */
|
||||
const formattedNumbers = numbersArray.map(number => ({
|
||||
formattedNumber: number
|
||||
}));
|
||||
/* eslint-enable arrow-body-style */
|
||||
|
||||
numbers[country] = formattedNumbers;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
|
||||
Object.keys(numbers).forEach((countryName: string) => {
|
||||
const numbersArray = numbers[countryName];
|
||||
|
||||
rows.push(
|
||||
<tr
|
||||
className = 'number-group'
|
||||
key = { countryName }>
|
||||
{ this._renderFlag(numbersArray[0].countryCode) }
|
||||
<td className = 'country' >{ countryName }</td>
|
||||
<td className = 'numbers-list-column'>
|
||||
{ this._renderNumbersList(numbersArray) }
|
||||
</td>
|
||||
<td className = 'toll-free-list-column' >
|
||||
{ this._renderNumbersTollFreeList(numbersArray) }
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<table className = 'dial-in-numbers-list'>
|
||||
<thead>
|
||||
<tr>
|
||||
{ hasFlags ? <th /> : null}
|
||||
<th>{ t('info.country') }</th>
|
||||
<th>{ t('info.numbers') }</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className = 'dial-in-numbers-body'>
|
||||
{ rows }
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a div container for a flag for the country of the phone number.
|
||||
*
|
||||
* @param {string} countryCode - The country code flag to display.
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderFlag(countryCode) {
|
||||
if (countryCode) {
|
||||
return (
|
||||
<td className = 'flag-cell'>
|
||||
{countryCode === 'SIP'
|
||||
? <Icon src = { IconPhoneRinging } />
|
||||
: <i className = { `flag iti-flag ${countryCode}` } />
|
||||
}
|
||||
</td>);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a div container for a phone number.
|
||||
*
|
||||
* @param {Array} numbers - The phone number to display.
|
||||
* @private
|
||||
* @returns {ReactElement[]}
|
||||
*/
|
||||
_renderNumbersList(numbers) {
|
||||
const numbersListItems = numbers.map(number =>
|
||||
(<li
|
||||
className = 'dial-in-number'
|
||||
key = { number.formattedNumber }>
|
||||
{ this._renderNumberLink(number.formattedNumber) }
|
||||
</li>));
|
||||
|
||||
return (
|
||||
<ul className = 'numbers-list'>
|
||||
{ numbersListItems }
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders list with a toll free text on the position where there is a
|
||||
* number marked as toll free.
|
||||
*
|
||||
* @param {Array} numbers - The phone number that are displayed.
|
||||
* @private
|
||||
* @returns {ReactElement[]}
|
||||
*/
|
||||
_renderNumbersTollFreeList(numbers) {
|
||||
const { t } = this.props;
|
||||
|
||||
const tollNumbersListItems = numbers.map(number =>
|
||||
(<li
|
||||
className = 'toll-free'
|
||||
key = { number.formattedNumber }>
|
||||
{ number.tollFree ? t('info.dialInTollFree') : '' }
|
||||
</li>));
|
||||
|
||||
return (
|
||||
<ul className = 'toll-free-list'>
|
||||
{ tollNumbersListItems }
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a ReactElement for displaying a telephone number. If the
|
||||
* component prop {@code clickableNumbers} is true, then the number will
|
||||
* have a link with the telephone protocol.
|
||||
*
|
||||
* @param {string} number - The phone number to display.
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderNumberLink(number) {
|
||||
if (this.props.clickableNumbers) {
|
||||
// Url encode # to %23, Android phone was cutting the # after
|
||||
// clicking it.
|
||||
// Seems that using ',' and '%23' works on iOS and Android.
|
||||
return (
|
||||
<a
|
||||
href = { `tel:${number},${this.props.conferenceID}%23` }
|
||||
key = { number } >
|
||||
{ number }
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return number;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default translate(NumbersList);
|
|
@ -0,0 +1,209 @@
|
|||
/* eslint-disable lines-around-comment */
|
||||
import countries from 'i18n-iso-countries';
|
||||
import en from 'i18n-iso-countries/langs/en.json';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
// @ts-ignore
|
||||
import { Icon, IconSip } from '../../../../base/icons';
|
||||
|
||||
countries.registerLocale(en);
|
||||
|
||||
interface INormalizedNumber {
|
||||
|
||||
/**
|
||||
* The country code.
|
||||
*/
|
||||
countryCode?: string;
|
||||
|
||||
/**
|
||||
* The formatted number.
|
||||
*/
|
||||
formattedNumber: string;
|
||||
|
||||
/**
|
||||
* Whether the number is toll-free.
|
||||
*/
|
||||
tollFree?: boolean;
|
||||
}
|
||||
|
||||
interface INumbersMapping {
|
||||
[countryName: string]: Array<INormalizedNumber>;
|
||||
}
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Whether or not numbers should include links with the telephone protocol.
|
||||
*/
|
||||
clickableNumbers: boolean;
|
||||
|
||||
/**
|
||||
* The conference ID for dialing in.
|
||||
*/
|
||||
conferenceID: number;
|
||||
|
||||
/**
|
||||
* The phone numbers to display. Can be an array of number Objects or an
|
||||
* object with countries as keys and an array of numbers as values.
|
||||
*/
|
||||
numbers: INumbersMapping;
|
||||
|
||||
}
|
||||
|
||||
const NumbersList: React.FC<IProps> = ({ t, conferenceID, clickableNumbers, numbers: numbersMapping }) => {
|
||||
const renderFlag = useCallback((countryCode: string) => {
|
||||
if (countryCode) {
|
||||
return (
|
||||
<td className = 'flag-cell'>
|
||||
{countryCode === 'SIP'
|
||||
? <Icon src = { IconSip } />
|
||||
: <i className = { `flag iti-flag ${countryCode}` } />
|
||||
}
|
||||
</td>);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
const renderNumberLink = useCallback((number: string) => {
|
||||
if (clickableNumbers) {
|
||||
// Url encode # to %23, Android phone was cutting the # after
|
||||
// clicking it.
|
||||
// Seems that using ',' and '%23' works on iOS and Android.
|
||||
return (
|
||||
<a
|
||||
href = { `tel:${number},${conferenceID}%23` }
|
||||
key = { number } >
|
||||
{number}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return number;
|
||||
}, [ conferenceID, clickableNumbers ]);
|
||||
|
||||
const renderNumbersList = useCallback((numbers: Array<INormalizedNumber>) => {
|
||||
const numbersListItems = numbers.map(number =>
|
||||
(<li
|
||||
className = 'dial-in-number'
|
||||
key = { number.formattedNumber }>
|
||||
{renderNumberLink(number.formattedNumber)}
|
||||
</li>));
|
||||
|
||||
return (
|
||||
<ul className = 'numbers-list'>
|
||||
{numbersListItems}
|
||||
</ul>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const renderNumbersTollFreeList = useCallback((numbers: Array<INormalizedNumber>) => {
|
||||
const tollNumbersListItems = numbers.map(number =>
|
||||
(<li
|
||||
className = 'toll-free'
|
||||
key = { number.formattedNumber }>
|
||||
{number.tollFree ? t('info.dialInTollFree') : ''}
|
||||
</li>));
|
||||
|
||||
return (
|
||||
<ul className = 'toll-free-list'>
|
||||
{tollNumbersListItems}
|
||||
</ul>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const renderNumbers = useMemo(() => {
|
||||
let numbers: INumbersMapping;
|
||||
|
||||
if (!numbersMapping) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(numbersMapping)) {
|
||||
numbers = numbersMapping.reduce(
|
||||
(resultNumbers: any, number: any) => {
|
||||
// The i18n-iso-countries package insists on upper case.
|
||||
const countryCode = number.countryCode.toUpperCase();
|
||||
let countryName;
|
||||
|
||||
if (countryCode === 'SIP') {
|
||||
countryName = t('info.sip');
|
||||
} else {
|
||||
countryName = t(`countries:countries.${countryCode}`);
|
||||
|
||||
// Some countries have multiple names as US ['United States of America', 'USA']
|
||||
// choose the first one if that is the case
|
||||
if (!countryName) {
|
||||
countryName = t(`countries:countries.${countryCode}.0`);
|
||||
}
|
||||
}
|
||||
|
||||
if (resultNumbers[countryName]) {
|
||||
resultNumbers[countryName].push(number);
|
||||
} else {
|
||||
resultNumbers[countryName] = [ number ];
|
||||
}
|
||||
|
||||
return resultNumbers;
|
||||
}, {});
|
||||
} else {
|
||||
numbers = {};
|
||||
|
||||
for (const [ country, numbersArray ]
|
||||
of Object.entries(numbersMapping.numbers)) {
|
||||
|
||||
if (Array.isArray(numbersArray)) {
|
||||
/* eslint-disable arrow-body-style */
|
||||
const formattedNumbers = numbersArray.map(number => ({
|
||||
formattedNumber: number
|
||||
}));
|
||||
/* eslint-enable arrow-body-style */
|
||||
|
||||
numbers[country] = formattedNumbers;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rows: [JSX.Element] = [] as unknown as [JSX.Element];
|
||||
|
||||
Object.keys(numbers).forEach((countryName: string) => {
|
||||
const numbersArray: Array<INormalizedNumber> = numbers[countryName];
|
||||
const countryCode = numbersArray[0].countryCode
|
||||
|| countries.getAlpha2Code(countryName, 'en')?.toUpperCase()
|
||||
|| countryName;
|
||||
|
||||
rows.push(
|
||||
<>
|
||||
<tr
|
||||
key = { countryName }>
|
||||
{renderFlag(countryCode)}
|
||||
<td className = 'country' >{countryName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td />
|
||||
<td className = 'numbers-list-column'>
|
||||
{renderNumbersList(numbersArray)}
|
||||
</td>
|
||||
<td className = 'toll-free-list-column' >
|
||||
{renderNumbersTollFreeList(numbersArray)}
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
return rows;
|
||||
}, [ numbersMapping ]);
|
||||
|
||||
return (
|
||||
<table className = 'dial-in-numbers-list'>
|
||||
<tbody className = 'dial-in-numbers-body'>
|
||||
{renderNumbers}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default translate(NumbersList);
|
|
@ -19,6 +19,7 @@ import { getDefaultURL } from '../../../app/functions.native';
|
|||
import { IReduxState } from '../../../app/types';
|
||||
// @ts-ignore
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import { getLegalUrls } from '../../../base/config/functions.native';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
// @ts-ignore
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
|
@ -46,19 +47,6 @@ import styles from './styles';
|
|||
*/
|
||||
const { AppInfo } = NativeModules;
|
||||
|
||||
/**
|
||||
* The URL at which the terms (of service/use) are available to the user.
|
||||
*/
|
||||
const TERMS_URL = 'https://jitsi.org/meet/terms';
|
||||
|
||||
/**
|
||||
* The URL at which the privacy policy is available to the user.
|
||||
*/
|
||||
const PRIVACY_URL = 'https://jitsi.org/meet/privacy';
|
||||
|
||||
|
||||
const DEFAULT_HELP_CENTRE_URL = 'https://web-cdn.jitsi.net/faq/meet-faq.html';
|
||||
|
||||
interface IState {
|
||||
|
||||
/**
|
||||
|
@ -119,11 +107,13 @@ interface IState {
|
|||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The URL for when the help link.
|
||||
*
|
||||
* @protected
|
||||
* The legal URL's.
|
||||
*/
|
||||
_helpCentreUrl: string;
|
||||
_legalUrls: {
|
||||
helpCentre: string;
|
||||
privacy: string;
|
||||
terms: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The ID of the local participant.
|
||||
|
@ -710,7 +700,7 @@ class SettingsView extends Component<IProps, IState> {
|
|||
* @returns {void}
|
||||
*/
|
||||
_onShowHelpPressed() {
|
||||
Linking.openURL(this.props._helpCentreUrl);
|
||||
Linking.openURL(this.props._legalUrls.helpCentre);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -719,7 +709,7 @@ class SettingsView extends Component<IProps, IState> {
|
|||
* @returns {void}
|
||||
*/
|
||||
_onShowPrivacyPressed() {
|
||||
Linking.openURL(PRIVACY_URL);
|
||||
Linking.openURL(this.props._legalUrls.privacy);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -728,7 +718,7 @@ class SettingsView extends Component<IProps, IState> {
|
|||
* @returns {void}
|
||||
*/
|
||||
_onShowTermsPressed() {
|
||||
Linking.openURL(TERMS_URL);
|
||||
Linking.openURL(this.props._legalUrls.terms);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -795,7 +785,7 @@ function _mapStateToProps(state: IReduxState) {
|
|||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
_helpCentreUrl: state['features/base/config'].helpCentreURL || DEFAULT_HELP_CENTRE_URL,
|
||||
_legalUrls: getLegalUrls(state),
|
||||
_localParticipantId: localParticipant?.id,
|
||||
_serverURL: getDefaultURL(state),
|
||||
_serverURLChangeEnabled: isServerURLChangeEnabled(state),
|
||||
|
|
|
@ -7,6 +7,7 @@ import { App } from './features/app/components';
|
|||
import { getLogger } from './features/base/logging/functions';
|
||||
import { Platform } from './features/base/react';
|
||||
import { getJitsiMeetGlobalNS } from './features/base/util';
|
||||
import DialInSummaryApp from './features/invite/components/dial-in-summary/web/DialInSummaryApp';
|
||||
import PrejoinApp from './features/prejoin/components/web/PrejoinApp';
|
||||
|
||||
const logger = getLogger('index.web');
|
||||
|
@ -43,7 +44,8 @@ const globalNS = getJitsiMeetGlobalNS();
|
|||
|
||||
globalNS.entryPoints = {
|
||||
APP: App,
|
||||
PREJOIN: PrejoinApp
|
||||
PREJOIN: PrejoinApp,
|
||||
DIALIN: DialInSummaryApp
|
||||
};
|
||||
|
||||
globalNS.renderEntryPoint = ({
|
||||
|
|
|
@ -1,17 +1,29 @@
|
|||
<html>
|
||||
<html xmlns="http://www.w3.org/1999/html">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!--#include virtual="/base.html" -->
|
||||
<!--#include virtual="/title.html" -->
|
||||
|
||||
<link rel="stylesheet" href="css/all.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="react"></div>
|
||||
<script>
|
||||
window.EXCALIDRAW_ASSET_PATH = 'libs/';
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!JitsiMeetJS.app) {
|
||||
return;
|
||||
}
|
||||
|
||||
JitsiMeetJS.app.renderEntryPoint({
|
||||
Component: JitsiMeetJS.app.entryPoints.DIALIN
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<!--#include virtual="/title.html" -->
|
||||
<script><!--#include virtual="/config.js" --></script>
|
||||
<script><!--#include virtual="/interface_config.js" --></script>
|
||||
<script src="libs/dial_in_info_bundle.min.js"></script>
|
||||
<script src="libs/lib-jitsi-meet.min.js?v=139"></script>
|
||||
<script src="libs/app.bundle.min.js?v=139"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="react" role="main"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -322,20 +322,6 @@ module.exports = (_env, argv) => {
|
|||
],
|
||||
performance: getPerformanceHints(perfHintOptions, 800 * 1024)
|
||||
}),
|
||||
Object.assign({}, config, {
|
||||
entry: {
|
||||
'dial_in_info_bundle': './react/features/invite/components/dial-in-info-page'
|
||||
},
|
||||
plugins: [
|
||||
...config.plugins,
|
||||
...getBundleAnalyzerPlugin(analyzeBundle, 'dial_in_info'),
|
||||
new webpack.IgnorePlugin({
|
||||
resourceRegExp: /^\.\/locale$/,
|
||||
contextRegExp: /moment$/
|
||||
})
|
||||
],
|
||||
performance: getPerformanceHints(perfHintOptions, 500 * 1024)
|
||||
}),
|
||||
Object.assign({}, config, {
|
||||
entry: {
|
||||
'do_external_connect': './connection_optimization/do_external_connect.js'
|
||||
|
|
Loading…
Reference in New Issue