This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import { Route } from '@react-navigation/native';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { Linking, View, ViewStyle } from 'react-native';
|
||||
import { WebView } from 'react-native-webview';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IStore } from '../../../../app/types';
|
||||
import { openDialog } from '../../../../base/dialog/actions';
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import JitsiScreen from '../../../../base/modal/components/JitsiScreen';
|
||||
import LoadingIndicator from '../../../../base/react/components/native/LoadingIndicator';
|
||||
import { getDialInfoPageURLForURIString } from '../../../functions';
|
||||
|
||||
import DialInSummaryErrorDialog from './DialInSummaryErrorDialog';
|
||||
import styles, { INDICATOR_COLOR } from './styles';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Default prop for navigating between screen components(React Navigation).
|
||||
*/
|
||||
navigation: any;
|
||||
|
||||
/**
|
||||
* Default prop for navigating between screen components(React Navigation).
|
||||
*/
|
||||
route: Route<'', { summaryUrl: string; }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React native component that displays the dial in info page for a specific room.
|
||||
*/
|
||||
class DialInSummary extends PureComponent<IProps> {
|
||||
|
||||
/**
|
||||
* Initializes a new instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._onError = this._onError.bind(this);
|
||||
this._onNavigate = this._onNavigate.bind(this);
|
||||
this._renderLoading = this._renderLoading.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount()}. Invoked
|
||||
* immediately after mounting occurs.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentDidMount() {
|
||||
const { navigation, t } = this.props;
|
||||
|
||||
navigation.setOptions({
|
||||
headerTitle: t('dialIn.screenTitle')
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { route } = this.props;
|
||||
const summaryUrl = route.params?.summaryUrl;
|
||||
|
||||
return (
|
||||
<JitsiScreen
|
||||
style = { styles.backDrop }>
|
||||
<WebView
|
||||
incognito = { true }
|
||||
onError = { this._onError }
|
||||
onShouldStartLoadWithRequest = { this._onNavigate }
|
||||
renderLoading = { this._renderLoading }
|
||||
setSupportMultipleWindows = { false }
|
||||
source = {{ uri: getDialInfoPageURLForURIString(summaryUrl) ?? '' }}
|
||||
startInLoadingState = { true }
|
||||
style = { styles.webView }
|
||||
webviewDebuggingEnabled = { true } />
|
||||
</JitsiScreen>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to handle the error if the page fails to load.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onError() {
|
||||
this.props.dispatch(openDialog(DialInSummaryErrorDialog));
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to intercept navigation inside the webview and make the native app handle the dial requests.
|
||||
*
|
||||
* NOTE: We don't navigate to anywhere else form that view.
|
||||
*
|
||||
* @param {any} request - The request object.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onNavigate(request: { url: string; }) {
|
||||
const { url } = request;
|
||||
const { route } = this.props;
|
||||
const summaryUrl = route.params?.summaryUrl;
|
||||
|
||||
if (url.startsWith('tel:')) {
|
||||
Linking.openURL(url);
|
||||
}
|
||||
|
||||
return url === getDialInfoPageURLForURIString(summaryUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the loading indicator.
|
||||
*
|
||||
* @returns {React$Component<any>}
|
||||
*/
|
||||
_renderLoading() {
|
||||
return (
|
||||
<View style = { styles.indicatorWrapper as ViewStyle }>
|
||||
<LoadingIndicator
|
||||
color = { INDICATOR_COLOR }
|
||||
size = 'large' />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(DialInSummary));
|
||||
@@ -0,0 +1,26 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import AlertDialog from '../../../../base/dialog/components/native/AlertDialog';
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
|
||||
/**
|
||||
* Dialog to inform the user that we couldn't fetch the dial-in info page.
|
||||
*/
|
||||
class DialInSummaryErrorDialog extends Component<WithTranslation> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<AlertDialog
|
||||
contentKey = 'info.dialInSummaryError' />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(DialInSummaryErrorDialog));
|
||||
@@ -0,0 +1,25 @@
|
||||
import BaseTheme from '../../../../base/ui/components/BaseTheme.native';
|
||||
|
||||
export const INDICATOR_COLOR = BaseTheme.palette.ui07;
|
||||
|
||||
const WV_BACKGROUND = BaseTheme.palette.ui03;
|
||||
|
||||
export default {
|
||||
|
||||
backDrop: {
|
||||
backgroundColor: WV_BACKGROUND,
|
||||
flex: 1
|
||||
},
|
||||
|
||||
indicatorWrapper: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.ui10,
|
||||
height: '100%',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
webView: {
|
||||
backgroundColor: WV_BACKGROUND,
|
||||
flex: 1
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
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 { _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,
|
||||
|
||||
'& *': {
|
||||
userSelect: 'text'
|
||||
}
|
||||
},
|
||||
confNameLabel: {
|
||||
...theme.typography.heading6,
|
||||
marginBottom: 18,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
},
|
||||
descriptionLabel: {
|
||||
...theme.typography.bodyShortRegularLarge,
|
||||
marginBottom: 18
|
||||
},
|
||||
separator: {
|
||||
width: '100%',
|
||||
height: 1,
|
||||
background: theme.palette.ui04,
|
||||
marginBottom: 18
|
||||
},
|
||||
pinLabel: {
|
||||
...theme.typography.heading6
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const ConferenceID: React.FC<IProps> = ({ conferenceID, t }) => {
|
||||
const { classes: styles } = useStyles();
|
||||
|
||||
return (
|
||||
<div className = { styles.container }>
|
||||
<div className = { styles.descriptionLabel }>
|
||||
{ t('info.dialANumber') }
|
||||
</div>
|
||||
<div className = { styles.separator } />
|
||||
<div className = { styles.pinLabel }>
|
||||
{ `${t('info.dialInConferenceID')} ${_formatConferenceIDPin(conferenceID ?? '')}` }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default translate(ConferenceID);
|
||||
@@ -0,0 +1,312 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import clsx from 'clsx';
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { withStyles } from 'tss-react/mui';
|
||||
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import { getDialInConferenceID, getDialInNumbers } from '../../../_utils';
|
||||
|
||||
import ConferenceID from './ConferenceID';
|
||||
import NumbersList from './NumbersList';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link DialInSummary}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Additional CSS classnames to append to the root of the component.
|
||||
*/
|
||||
className: string;
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
*/
|
||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
||||
|
||||
/**
|
||||
* Whether or not numbers should include links with the telephone protocol.
|
||||
*/
|
||||
clickableNumbers: boolean;
|
||||
|
||||
/**
|
||||
* Whether to hide the error.
|
||||
*/
|
||||
hideError?: boolean;
|
||||
|
||||
/**
|
||||
* The name of the conference to show a conferenceID for.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
url: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link DialInSummary}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The numeric ID of the conference, used as a pin when dialing in.
|
||||
*/
|
||||
conferenceID: string | null;
|
||||
|
||||
/**
|
||||
* An error message to display.
|
||||
*/
|
||||
error: string;
|
||||
|
||||
/**
|
||||
* Whether or not the app is fetching data.
|
||||
*/
|
||||
loading: boolean;
|
||||
|
||||
/**
|
||||
* The dial-in numbers to be displayed.
|
||||
*/
|
||||
numbers: Array<Object> | Object | null;
|
||||
|
||||
/**
|
||||
* Whether or not dial-in is allowed.
|
||||
*/
|
||||
numbersEnabled: boolean | null;
|
||||
};
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
hasNumbers: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
background: '#1E1E1E',
|
||||
color: theme.palette.text01
|
||||
},
|
||||
scrollable: {
|
||||
height: '100dvh',
|
||||
overflowY: 'scroll' as const
|
||||
},
|
||||
roomName: {
|
||||
margin: '40px auto 8px',
|
||||
...theme.typography.heading5
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays a page listing numbers for dialing into a conference and pin to
|
||||
* the a specific conference.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class DialInSummary extends Component<IProps, State> {
|
||||
override state = {
|
||||
conferenceID: null,
|
||||
error: '',
|
||||
loading: true,
|
||||
numbers: null,
|
||||
numbersEnabled: null
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code DialInSummary} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onGetNumbersSuccess = this._onGetNumbersSuccess.bind(this);
|
||||
this._onGetConferenceIDSuccess
|
||||
= this._onGetConferenceIDSuccess.bind(this);
|
||||
this._setErrorMessage = this._setErrorMessage.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@link Component#componentDidMount()}. Invoked immediately
|
||||
* after this component is mounted.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentDidMount() {
|
||||
const getNumbers = this._getNumbers()
|
||||
.then(this._onGetNumbersSuccess)
|
||||
.catch(this._setErrorMessage);
|
||||
|
||||
const getID = this._getConferenceID()
|
||||
.then(this._onGetConferenceIDSuccess)
|
||||
.catch(this._setErrorMessage);
|
||||
|
||||
Promise.all([ getNumbers, getID ])
|
||||
.then(() => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
let className = '';
|
||||
let contents;
|
||||
|
||||
const { conferenceID, error, loading, numbersEnabled } = this.state;
|
||||
const { hideError, showTitle, room, clickableNumbers, scrollable, t } = this.props;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
if (loading) {
|
||||
contents = '';
|
||||
} else if (numbersEnabled === false) {
|
||||
contents = t('info.dialInNotSupported');
|
||||
} else if (error) {
|
||||
if (!hideError) {
|
||||
contents = error;
|
||||
}
|
||||
} else {
|
||||
className = clsx(classes.hasNumbers, scrollable && classes.scrollable);
|
||||
contents = [
|
||||
conferenceID
|
||||
? <>
|
||||
{ showTitle && <div className = { classes.roomName }>{ room }</div> }
|
||||
<ConferenceID
|
||||
conferenceID = { conferenceID }
|
||||
conferenceName = { room }
|
||||
key = 'conferenceID' />
|
||||
</> : null,
|
||||
<NumbersList
|
||||
clickableNumbers = { clickableNumbers }
|
||||
conferenceID = { conferenceID }
|
||||
key = 'numbers'
|
||||
numbers = { this.state.numbers } />
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = { className }>
|
||||
{ contents }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an AJAX request for the conference ID.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_getConferenceID() {
|
||||
const { room } = this.props;
|
||||
const { dialInConfCodeUrl, hosts } = config;
|
||||
const mucURL = hosts?.muc;
|
||||
|
||||
if (!dialInConfCodeUrl || !mucURL || !room) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
let url = this.props.url || {};
|
||||
|
||||
if (typeof url === 'string' || url instanceof String) {
|
||||
// @ts-ignore
|
||||
url = new URL(url);
|
||||
}
|
||||
|
||||
return getDialInConferenceID(dialInConfCodeUrl, room, mucURL, url)
|
||||
.catch(() => Promise.reject(this.props.t('info.genericError')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an AJAX request for dial-in numbers.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_getNumbers() {
|
||||
const { room } = this.props;
|
||||
const { dialInNumbersUrl, hosts } = config;
|
||||
const mucURL = hosts?.muc;
|
||||
|
||||
if (!dialInNumbersUrl) {
|
||||
return Promise.reject(this.props.t('info.dialInNotSupported'));
|
||||
}
|
||||
|
||||
return getDialInNumbers(dialInNumbersUrl, room, mucURL ?? '')
|
||||
.catch(() => Promise.reject(this.props.t('info.genericError')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked when fetching the conference ID succeeds.
|
||||
*
|
||||
* @param {Object} response - The response from fetching the conference ID.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onGetConferenceIDSuccess(response = { conference: undefined,
|
||||
id: undefined }) {
|
||||
const { conference, id } = response;
|
||||
|
||||
if (!conference || !id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ conferenceID: id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked when fetching dial-in numbers succeeds. Sets the
|
||||
* internal to show the numbers.
|
||||
*
|
||||
* @param {Array|Object} response - The response from fetching
|
||||
* dial-in numbers.
|
||||
* @param {Array|Object} response.numbers - The dial-in numbers.
|
||||
* @param {boolean} response.numbersEnabled - Whether or not dial-in is
|
||||
* enabled, old syntax that is deprecated.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onGetNumbersSuccess(
|
||||
response: Array<Object> | { numbersEnabled?: boolean; }) {
|
||||
|
||||
this.setState({
|
||||
numbersEnabled:
|
||||
Boolean(Array.isArray(response)
|
||||
? response.length > 0 : response.numbersEnabled),
|
||||
numbers: response
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an error message to display on the page instead of content.
|
||||
*
|
||||
* @param {string} error - The error message to display.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setErrorMessage(error: string) {
|
||||
this.setState({
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(withStyles(DialInSummary, styles));
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { ComponentType } from 'react';
|
||||
|
||||
import BaseApp from '../../../../base/app/components/BaseApp';
|
||||
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/parseURLParams';
|
||||
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<any> {
|
||||
/**
|
||||
* Navigates to {@link Prejoin} upon mount.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
override async componentDidMount() {
|
||||
await super.componentDidMount();
|
||||
|
||||
// @ts-ignore
|
||||
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
|
||||
*/
|
||||
override _createMainElement(component: ComponentType<any>, props: Object) {
|
||||
return (
|
||||
<JitsiThemeProvider>
|
||||
<GlobalStyles />
|
||||
{super._createMainElement(component, props)}
|
||||
</JitsiThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the platform specific dialog container.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
override _renderDialogContainer() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
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';
|
||||
import Icon from '../../../../base/icons/components/Icon';
|
||||
import { IconSip } from '../../../../base/icons/svg';
|
||||
|
||||
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 | null;
|
||||
|
||||
/**
|
||||
* 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 | null;
|
||||
|
||||
}
|
||||
|
||||
const NumbersList: React.FC<IProps> = ({ t, conferenceID, clickableNumbers, numbers: numbersMapping }) => {
|
||||
const renderFlag = useCallback((countryCode: string) => {
|
||||
if (countryCode) {
|
||||
return (
|
||||
<td className = 'flag-cell'>
|
||||
{countryCode === 'SIP' || countryCode === 'SIP_AUDIO_ONLY'
|
||||
? <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 if (countryCode === 'SIP_AUDIO_ONLY') {
|
||||
countryName = t('info.sipAudioOnly');
|
||||
} 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);
|
||||
Reference in New Issue
Block a user