mirror of https://github.com/jitsi/jitsi-meet
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.pull/12956/head jitsi-meet_8338
parent
9b7a5ffdd1
commit
35ee92869f
After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 6.3 KiB |
@ -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); |
@ -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); |
@ -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> |
||||
|
Loading…
Reference in new issue