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:
Horatiu Muresan 2023-02-23 17:01:55 +02:00 committed by GitHub
parent 9b7a5ffdd1
commit 35ee92869f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 925 additions and 888 deletions

View File

@ -48,8 +48,6 @@ deploy-appbundle:
$(BUILD_DIR)/do_external_connect.min.js.map \ $(BUILD_DIR)/do_external_connect.min.js.map \
$(BUILD_DIR)/external_api.min.js \ $(BUILD_DIR)/external_api.min.js \
$(BUILD_DIR)/external_api.min.js.map \ $(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 \
$(BUILD_DIR)/alwaysontop.min.js.map \ $(BUILD_DIR)/alwaysontop.min.js.map \
$(OUTPUT_DIR)/analytics-ga.js \ $(OUTPUT_DIR)/analytics-ga.js \

View File

@ -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 // A property to disable the right click context menu for localVideo
// the menu has option to flip the locally seen video for local presentations // the menu has option to flip the locally seen video for local presentations
// disableLocalVideoFlip: false, // disableLocalVideoFlip: false,

View File

@ -201,11 +201,6 @@ $deepLinkingDialInConferenceIdPadding: inherit;
$deepLinkingDialInConferenceIdBackgroundColor: inherit; $deepLinkingDialInConferenceIdBackgroundColor: inherit;
$deepLinkingDialInConferenceIdBorderRadius: inherit; $deepLinkingDialInConferenceIdBorderRadius: inherit;
$deepLinkingDialInConferenceNameFontSize: inherit;
$deepLinkingDialInConferenceNameLineHeight: inherit;
$deepLinkingDialInConferenceNameMarginBottom: none;
$deepLinkingDialInConferenceNameFontWeight: inherit;
$deepLinkingDialInConferenceDescriptionFontSize: 0.8em; $deepLinkingDialInConferenceDescriptionFontSize: 0.8em;
$deepLinkingDialInConferenceDescriptionLineHeight: inherit; $deepLinkingDialInConferenceDescriptionLineHeight: inherit;
$deepLinkingDialInConferenceDescriptionMarginBottom: none; $deepLinkingDialInConferenceDescriptionMarginBottom: none;

View File

@ -67,6 +67,13 @@
font-size: 1em; font-size: 1em;
} }
.dial-in-conference-id {
text-align: center;
min-width: 200px;
margin-top: 40px;
}
.dial-in-conference-id { .dial-in-conference-id {
margin: $deepLinkingDialInConferenceIdMargin; margin: $deepLinkingDialInConferenceIdMargin;
padding: $deepLinkingDialInConferenceIdPadding; padding: $deepLinkingDialInConferenceIdPadding;
@ -74,24 +81,12 @@
border-radius: $deepLinkingDialInConferenceIdBorderRadius; border-radius: $deepLinkingDialInConferenceIdBorderRadius;
} }
.dial-in-conference-name {
font-size: $deepLinkingDialInConferenceNameFontSize;
line-height: $deepLinkingDialInConferenceNameLineHeight;
margin-bottom: $deepLinkingDialInConferenceNameMarginBottom;
font-weight: $deepLinkingDialInConferenceNameFontWeight;
}
.dial-in-conference-description { .dial-in-conference-description {
font-size: $deepLinkingDialInConferenceDescriptionFontSize; font-size: $deepLinkingDialInConferenceDescriptionFontSize;
line-height: $deepLinkingDialInConferenceDescriptionLineHeight; line-height: $deepLinkingDialInConferenceDescriptionLineHeight;
margin-bottom: $deepLinkingDialInConferenceDescriptionMarginBottom; margin-bottom: $deepLinkingDialInConferenceDescriptionMarginBottom;
} }
.dial-in-conference-pin {
font-size: $deepLinkingDialInConferencePinFontSize;
line-height: $deepLinkingDialInConferencePinLineHeight;
}
.toll-free-list { .toll-free-list {
min-width: 80px; min-width: 80px;
} }

View File

@ -50,6 +50,8 @@
} }
.dial-in-numbers-list { .dial-in-numbers-list {
max-width: 334px;
width: 100%;
margin-top: 20px; margin-top: 20px;
font-size: 12px; font-size: 12px;
line-height: 24px; line-height: 24px;
@ -59,10 +61,6 @@
text-align: left; text-align: left;
} }
tr {
border-bottom: 1px solid #d1dbe8;
}
.flag-cell { .flag-cell {
vertical-align: top; vertical-align: top;
width: 30px; width: 30px;
@ -91,6 +89,7 @@
font-weight: bold; font-weight: bold;
list-style: none; list-style: none;
vertical-align: top; vertical-align: top;
text-align: right;
} }
li.toll-free:empty:before { li.toll-free:empty:before {
@ -119,11 +118,6 @@
margin-top: 40px; margin-top: 40px;
} }
.dial-in-conference-name,
.dial-in-conference-pin {
font-size: 18px;
}
.dial-in-conference-description { .dial-in-conference-description {
margin: 12px; 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

View File

@ -184,13 +184,21 @@
"deepLinking": { "deepLinking": {
"appNotInstalled": "You need the {{app}} mobile app to join this meeting on your phone.", "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.", "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.", "descriptionWithoutWeb": "Nothing happened? We tried launching your meeting in the {{app}} desktop app.",
"downloadApp": "Download the app", "downloadApp": "Download the app",
"downloadMobileApp": "Download from App Store",
"ifDoNotHaveApp": "If you don't have the app yet:", "ifDoNotHaveApp": "If you don't have the app yet:",
"ifHaveApp": "If you already have the app:", "ifHaveApp": "If you already have the app:",
"joinInApp": "Join this meeting using 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", "launchWebButton": "Launch in web",
"noMobileApp": "You dont 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}}...", "title": "Launching your meeting in {{app}}...",
"titleNew": "Launching your meeting ...",
"tryAgainButton": "Try again in desktop", "tryAgainButton": "Try again in desktop",
"unsupportedBrowser": "It looks like you're using a browser we don't support." "unsupportedBrowser": "It looks like you're using a browser we don't support."
}, },

View File

@ -388,6 +388,11 @@ export interface IConfig {
lastNLimits?: { lastNLimits?: {
[key: number]: number; [key: number]: number;
}; };
legalUrls?: {
helpCentre: string;
privacy: string;
terms: string;
};
liveStreaming?: { liveStreaming?: {
dataPrivacyLink?: string; dataPrivacyLink?: string;
enabled?: boolean; enabled?: boolean;

View File

@ -70,3 +70,18 @@ export const THIRD_PARTY_PREJOIN_BUTTONS = [ 'microphone', 'camera', 'select-bac
export const FEATURE_FLAGS = { export const FEATURE_FLAGS = {
SSRC_REWRITING: 'ssrcRewritingEnabled' 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';

View File

@ -11,7 +11,13 @@ import { parseURLParams } from '../util/parseURLParams';
import { IConfig } from './configType'; import { IConfig } from './configType';
import CONFIG_WHITELIST from './configWhitelist'; 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 INTERFACE_CONFIG_WHITELIST from './interfaceConfigWhitelist';
import logger from './logger'; import logger from './logger';
@ -326,3 +332,24 @@ export function getDialOutUrl(state: IReduxState) {
export function getSecurityUiConfig(state: IReduxState) { export function getSecurityUiConfig(state: IReduxState) {
return state['features/base/config']?.securityUi || {}; 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
};
}

View File

@ -1,5 +1,3 @@
// @flow
import type { Dispatch } from 'redux'; import type { Dispatch } from 'redux';
import { appNavigate } from '../app/actions'; import { appNavigate } from '../app/actions';

View File

@ -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));

View File

@ -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);

View File

@ -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));

View File

@ -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);

View File

@ -1,9 +0,0 @@
// @flow
/**
* Method used in order to render a custom promotional footer.
*
* @returns {HTMLElement}
*/
export function renderPromotionalFooter() {
return null;
}

View File

@ -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);

View File

@ -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);

View File

@ -1,8 +1,13 @@
// @flow // @flow
import { Theme } from '@mui/material';
import { withStyles } from '@mui/styles';
import clsx from 'clsx';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { translate } from '../../../../base/i18n'; import { translate } from '../../../../base/i18n';
import { withPixelLineHeight } from '../../../../base/styles/functions.web';
import { getDialInConferenceID, getDialInNumbers } from '../../../_utils'; import { getDialInConferenceID, getDialInNumbers } from '../../../_utils';
import ConferenceID from './ConferenceID'; import ConferenceID from './ConferenceID';
@ -20,6 +25,11 @@ type Props = {
*/ */
className: string, className: string,
/**
* An object containing the CSS classes.
*/
classes: any;
/** /**
* Whether or not numbers should include links with the telephone protocol. * Whether or not numbers should include links with the telephone protocol.
*/ */
@ -30,6 +40,16 @@ type Props = {
*/ */
room: string, 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. * The url where we were loaded.
*/ */
@ -72,6 +92,26 @@ type State = {
numbersEnabled: ?boolean 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 * Displays a page listing numbers for dialing into a conference and pin to
* the a specific conference. * the a specific conference.
@ -136,24 +176,27 @@ class DialInSummary extends Component<Props, State> {
let contents; let contents;
const { conferenceID, error, loading, numbersEnabled } = this.state; const { conferenceID, error, loading, numbersEnabled } = this.state;
const { classes, showTitle, room, clickableNumbers, scrollable, t } = this.props;
if (loading) { if (loading) {
contents = ''; contents = '';
} else if (numbersEnabled === false) { } else if (numbersEnabled === false) {
contents = this.props.t('info.dialInNotSupported'); contents = t('info.dialInNotSupported');
} else if (error) { } else if (error) {
contents = error; contents = error;
} else { } else {
className = 'has-numbers'; className = clsx(classes.hasNumbers, scrollable && classes.scrollable);
contents = [ contents = [
conferenceID conferenceID
? <ConferenceID ? <>
{ showTitle && <div className = { classes.roomName }>{ room }</div> }
<ConferenceID
conferenceID = { conferenceID } conferenceID = { conferenceID }
conferenceName = { this.props.room } conferenceName = { room }
key = 'conferenceID' /> key = 'conferenceID' />
: null, </> : null,
<NumbersList <NumbersList
clickableNumbers = { this.props.clickableNumbers } clickableNumbers = { clickableNumbers }
conferenceID = { conferenceID } conferenceID = { conferenceID }
key = 'numbers' key = 'numbers'
numbers = { this.state.numbers } /> numbers = { this.state.numbers } />
@ -161,7 +204,7 @@ class DialInSummary extends Component<Props, State> {
} }
return ( return (
<div className = { `${this.props.className} ${className}` }> <div className = { className }>
{ contents } { contents }
</div> </div>
); );
@ -272,4 +315,4 @@ class DialInSummary extends Component<Props, State> {
} }
} }
export default translate(DialInSummary); export default translate(withStyles(styles)(DialInSummary));

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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);

View File

@ -19,6 +19,7 @@ import { getDefaultURL } from '../../../app/functions.native';
import { IReduxState } from '../../../app/types'; import { IReduxState } from '../../../app/types';
// @ts-ignore // @ts-ignore
import { Avatar } from '../../../base/avatar'; import { Avatar } from '../../../base/avatar';
import { getLegalUrls } from '../../../base/config/functions.native';
import { translate } from '../../../base/i18n/functions'; import { translate } from '../../../base/i18n/functions';
// @ts-ignore // @ts-ignore
import JitsiScreen from '../../../base/modal/components/JitsiScreen'; import JitsiScreen from '../../../base/modal/components/JitsiScreen';
@ -46,19 +47,6 @@ import styles from './styles';
*/ */
const { AppInfo } = NativeModules; 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 { interface IState {
/** /**
@ -119,11 +107,13 @@ interface IState {
interface IProps extends WithTranslation { interface IProps extends WithTranslation {
/** /**
* The URL for when the help link. * The legal URL's.
*
* @protected
*/ */
_helpCentreUrl: string; _legalUrls: {
helpCentre: string;
privacy: string;
terms: string;
};
/** /**
* The ID of the local participant. * The ID of the local participant.
@ -710,7 +700,7 @@ class SettingsView extends Component<IProps, IState> {
* @returns {void} * @returns {void}
*/ */
_onShowHelpPressed() { _onShowHelpPressed() {
Linking.openURL(this.props._helpCentreUrl); Linking.openURL(this.props._legalUrls.helpCentre);
} }
/** /**
@ -719,7 +709,7 @@ class SettingsView extends Component<IProps, IState> {
* @returns {void} * @returns {void}
*/ */
_onShowPrivacyPressed() { _onShowPrivacyPressed() {
Linking.openURL(PRIVACY_URL); Linking.openURL(this.props._legalUrls.privacy);
} }
/** /**
@ -728,7 +718,7 @@ class SettingsView extends Component<IProps, IState> {
* @returns {void} * @returns {void}
*/ */
_onShowTermsPressed() { _onShowTermsPressed() {
Linking.openURL(TERMS_URL); Linking.openURL(this.props._legalUrls.terms);
} }
/** /**
@ -795,7 +785,7 @@ function _mapStateToProps(state: IReduxState) {
const localParticipant = getLocalParticipant(state); const localParticipant = getLocalParticipant(state);
return { return {
_helpCentreUrl: state['features/base/config'].helpCentreURL || DEFAULT_HELP_CENTRE_URL, _legalUrls: getLegalUrls(state),
_localParticipantId: localParticipant?.id, _localParticipantId: localParticipant?.id,
_serverURL: getDefaultURL(state), _serverURL: getDefaultURL(state),
_serverURLChangeEnabled: isServerURLChangeEnabled(state), _serverURLChangeEnabled: isServerURLChangeEnabled(state),

View File

@ -7,6 +7,7 @@ import { App } from './features/app/components';
import { getLogger } from './features/base/logging/functions'; import { getLogger } from './features/base/logging/functions';
import { Platform } from './features/base/react'; import { Platform } from './features/base/react';
import { getJitsiMeetGlobalNS } from './features/base/util'; 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'; import PrejoinApp from './features/prejoin/components/web/PrejoinApp';
const logger = getLogger('index.web'); const logger = getLogger('index.web');
@ -43,7 +44,8 @@ const globalNS = getJitsiMeetGlobalNS();
globalNS.entryPoints = { globalNS.entryPoints = {
APP: App, APP: App,
PREJOIN: PrejoinApp PREJOIN: PrejoinApp,
DIALIN: DialInSummaryApp
}; };
globalNS.renderEntryPoint = ({ globalNS.renderEntryPoint = ({

View File

@ -1,17 +1,29 @@
<html> <html xmlns="http://www.w3.org/1999/html">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="content-type" content="text/html;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"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--#include virtual="/base.html" --> <!--#include virtual="/base.html" -->
<!--#include virtual="/title.html" -->
<link rel="stylesheet" href="css/all.css"> <link rel="stylesheet" href="css/all.css">
</head> <script>
<body> window.EXCALIDRAW_ASSET_PATH = 'libs/';
<div id="react"></div> 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="/config.js" --></script>
<script><!--#include virtual="/interface_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> </body>
</html> </html>

View File

@ -322,20 +322,6 @@ module.exports = (_env, argv) => {
], ],
performance: getPerformanceHints(perfHintOptions, 800 * 1024) 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, { Object.assign({}, config, {
entry: { entry: {
'do_external_connect': './connection_optimization/do_external_connect.js' 'do_external_connect': './connection_optimization/do_external_connect.js'