From 35ee92869fdbc9c71ad2cea3f4a341dbf7a4c664 Mon Sep 17 00:00:00 2001 From: Horatiu Muresan <39557534+horymury@users.noreply.github.com> Date: Thu, 23 Feb 2023 17:01:55 +0200 Subject: [PATCH] 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. --- Makefile | 2 - config.js | 7 + css/_variables.scss | 5 - css/deep-linking/_mobile.scss | 19 +- css/modals/invite/_info.scss | 12 +- images/logo-deep-linking-mobile.png | Bin 0 -> 1986 bytes images/logo-deep-linking.png | Bin 18584 -> 6436 bytes lang/main.json | 8 + react/features/base/config/configType.ts | 5 + react/features/base/config/constants.ts | 15 + react/features/base/config/functions.any.ts | 29 +- .../deep-linking/{actions.js => actions.ts} | 2 - .../components/DeepLinkingDesktopPage.web.js | 192 ----------- .../components/DeepLinkingDesktopPage.web.tsx | 159 ++++++++++ .../components/DeepLinkingMobilePage.web.js | 299 ------------------ .../components/DeepLinkingMobilePage.web.tsx | 241 ++++++++++++++ .../deep-linking/renderPromotionalFooter.js | 9 - .../dial-in-summary/web/ConferenceID.js | 60 ---- .../dial-in-summary/web/ConferenceID.tsx | 77 +++++ .../dial-in-summary/web/DialInSummary.js | 63 +++- .../dial-in-summary/web/DialInSummaryApp.js | 80 +++++ .../dial-in-summary/web/NumbersList.js | 244 -------------- .../dial-in-summary/web/NumbersList.tsx | 209 ++++++++++++ .../components/native/SettingsView.tsx | 32 +- react/index.web.js | 4 +- static/dialInInfo.html | 26 +- webpack.config.js | 14 - 27 files changed, 925 insertions(+), 888 deletions(-) create mode 100644 images/logo-deep-linking-mobile.png rename react/features/deep-linking/{actions.js => actions.ts} (98%) delete mode 100644 react/features/deep-linking/components/DeepLinkingDesktopPage.web.js create mode 100644 react/features/deep-linking/components/DeepLinkingDesktopPage.web.tsx delete mode 100644 react/features/deep-linking/components/DeepLinkingMobilePage.web.js create mode 100644 react/features/deep-linking/components/DeepLinkingMobilePage.web.tsx delete mode 100644 react/features/deep-linking/renderPromotionalFooter.js delete mode 100644 react/features/invite/components/dial-in-summary/web/ConferenceID.js create mode 100644 react/features/invite/components/dial-in-summary/web/ConferenceID.tsx create mode 100644 react/features/invite/components/dial-in-summary/web/DialInSummaryApp.js delete mode 100644 react/features/invite/components/dial-in-summary/web/NumbersList.js create mode 100644 react/features/invite/components/dial-in-summary/web/NumbersList.tsx diff --git a/Makefile b/Makefile index 040f8c9774..32d14a9d7e 100644 --- a/Makefile +++ b/Makefile @@ -48,8 +48,6 @@ deploy-appbundle: $(BUILD_DIR)/do_external_connect.min.js.map \ $(BUILD_DIR)/external_api.min.js \ $(BUILD_DIR)/external_api.min.js.map \ - $(BUILD_DIR)/dial_in_info_bundle.min.js \ - $(BUILD_DIR)/dial_in_info_bundle.min.js.map \ $(BUILD_DIR)/alwaysontop.min.js \ $(BUILD_DIR)/alwaysontop.min.js.map \ $(OUTPUT_DIR)/analytics-ga.js \ diff --git a/config.js b/config.js index 422d2f1b01..02647606ba 100644 --- a/config.js +++ b/config.js @@ -1146,6 +1146,13 @@ var config = { // } // }, + // // The terms, privacy and help centre URL's. + // legalUrls: { + // helpCentre: 'https://web-cdn.jitsi.net/faq/meet-faq.html', + // privacy: 'https://jitsi.org/meet/privacy', + // terms: 'https://jitsi.org/meet/terms' + // }, + // A property to disable the right click context menu for localVideo // the menu has option to flip the locally seen video for local presentations // disableLocalVideoFlip: false, diff --git a/css/_variables.scss b/css/_variables.scss index dfe48669b0..40fdebf487 100644 --- a/css/_variables.scss +++ b/css/_variables.scss @@ -201,11 +201,6 @@ $deepLinkingDialInConferenceIdPadding: inherit; $deepLinkingDialInConferenceIdBackgroundColor: inherit; $deepLinkingDialInConferenceIdBorderRadius: inherit; -$deepLinkingDialInConferenceNameFontSize: inherit; -$deepLinkingDialInConferenceNameLineHeight: inherit; -$deepLinkingDialInConferenceNameMarginBottom: none; -$deepLinkingDialInConferenceNameFontWeight: inherit; - $deepLinkingDialInConferenceDescriptionFontSize: 0.8em; $deepLinkingDialInConferenceDescriptionLineHeight: inherit; $deepLinkingDialInConferenceDescriptionMarginBottom: none; diff --git a/css/deep-linking/_mobile.scss b/css/deep-linking/_mobile.scss index bab9f9b8f6..0b93e45e89 100644 --- a/css/deep-linking/_mobile.scss +++ b/css/deep-linking/_mobile.scss @@ -67,6 +67,13 @@ font-size: 1em; } + + .dial-in-conference-id { + text-align: center; + min-width: 200px; + margin-top: 40px; + } + .dial-in-conference-id { margin: $deepLinkingDialInConferenceIdMargin; padding: $deepLinkingDialInConferenceIdPadding; @@ -74,24 +81,12 @@ border-radius: $deepLinkingDialInConferenceIdBorderRadius; } - .dial-in-conference-name { - font-size: $deepLinkingDialInConferenceNameFontSize; - line-height: $deepLinkingDialInConferenceNameLineHeight; - margin-bottom: $deepLinkingDialInConferenceNameMarginBottom; - font-weight: $deepLinkingDialInConferenceNameFontWeight; - } - .dial-in-conference-description { font-size: $deepLinkingDialInConferenceDescriptionFontSize; line-height: $deepLinkingDialInConferenceDescriptionLineHeight; margin-bottom: $deepLinkingDialInConferenceDescriptionMarginBottom; } - .dial-in-conference-pin { - font-size: $deepLinkingDialInConferencePinFontSize; - line-height: $deepLinkingDialInConferencePinLineHeight; - } - .toll-free-list { min-width: 80px; } diff --git a/css/modals/invite/_info.scss b/css/modals/invite/_info.scss index 7992206c8c..cf233f9b0b 100644 --- a/css/modals/invite/_info.scss +++ b/css/modals/invite/_info.scss @@ -50,6 +50,8 @@ } .dial-in-numbers-list { + max-width: 334px; + width: 100%; margin-top: 20px; font-size: 12px; line-height: 24px; @@ -59,10 +61,6 @@ text-align: left; } - tr { - border-bottom: 1px solid #d1dbe8; - } - .flag-cell { vertical-align: top; width: 30px; @@ -91,6 +89,7 @@ font-weight: bold; list-style: none; vertical-align: top; + text-align: right; } li.toll-free:empty:before { @@ -119,11 +118,6 @@ margin-top: 40px; } - .dial-in-conference-name, - .dial-in-conference-pin { - font-size: 18px; - } - .dial-in-conference-description { margin: 12px; } diff --git a/images/logo-deep-linking-mobile.png b/images/logo-deep-linking-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..4225c83d2ca64ed6f5159d344a9df0d909db8cf0 GIT binary patch literal 1986 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 f00000;IR7+Y$ODe65SQZ00000NkvXXu0mjf^gB
You can try again or launch it on web.", "descriptionWithoutWeb": "Nothing happened? We tried launching your meeting in the {{app}} desktop app.", "downloadApp": "Download the app", + "downloadMobileApp": "Download from App Store", "ifDoNotHaveApp": "If you don't have the app yet:", "ifHaveApp": "If you already have the app:", "joinInApp": "Join this meeting using the app", + "joinInAppNew": "Join in app", + "joinInBrowser": "Join in browser", + "launchMeetingLabel": "How do you want to join this meeting?", "launchWebButton": "Launch in web", + "noMobileApp": "You don’t have the app?", + "termsAndConditions": "By continuing you agree to our terms & conditions.", "title": "Launching your meeting in {{app}}...", + "titleNew": "Launching your meeting ...", "tryAgainButton": "Try again in desktop", "unsupportedBrowser": "It looks like you're using a browser we don't support." }, diff --git a/react/features/base/config/configType.ts b/react/features/base/config/configType.ts index 63dbbfbe8d..e2bbf9109e 100644 --- a/react/features/base/config/configType.ts +++ b/react/features/base/config/configType.ts @@ -388,6 +388,11 @@ export interface IConfig { lastNLimits?: { [key: number]: number; }; + legalUrls?: { + helpCentre: string; + privacy: string; + terms: string; + }; liveStreaming?: { dataPrivacyLink?: string; enabled?: boolean; diff --git a/react/features/base/config/constants.ts b/react/features/base/config/constants.ts index 34bd5f51e7..6358c1ecf2 100644 --- a/react/features/base/config/constants.ts +++ b/react/features/base/config/constants.ts @@ -70,3 +70,18 @@ export const THIRD_PARTY_PREJOIN_BUTTONS = [ 'microphone', 'camera', 'select-bac export const FEATURE_FLAGS = { SSRC_REWRITING: 'ssrcRewritingEnabled' }; + +/** + * The URL at which the terms (of service/use) are available to the user. + */ +export const DEFAULT_TERMS_URL = 'https://jitsi.org/meet/terms'; + +/** + * The URL at which the privacy policy is available to the user. + */ +export const DEFAULT_PRIVACY_URL = 'https://jitsi.org/meet/privacy'; + +/** + * The URL at which the help centre is available to the user. + */ +export const DEFAULT_HELP_CENTRE_URL = 'https://web-cdn.jitsi.net/faq/meet-faq.html'; diff --git a/react/features/base/config/functions.any.ts b/react/features/base/config/functions.any.ts index 0d71bd2cd8..3f8049f3b1 100644 --- a/react/features/base/config/functions.any.ts +++ b/react/features/base/config/functions.any.ts @@ -11,7 +11,13 @@ import { parseURLParams } from '../util/parseURLParams'; import { IConfig } from './configType'; import CONFIG_WHITELIST from './configWhitelist'; -import { FEATURE_FLAGS, _CONFIG_STORE_PREFIX } from './constants'; +import { + DEFAULT_HELP_CENTRE_URL, + DEFAULT_PRIVACY_URL, + DEFAULT_TERMS_URL, + FEATURE_FLAGS, + _CONFIG_STORE_PREFIX +} from './constants'; import INTERFACE_CONFIG_WHITELIST from './interfaceConfigWhitelist'; import logger from './logger'; @@ -326,3 +332,24 @@ export function getDialOutUrl(state: IReduxState) { export function getSecurityUiConfig(state: IReduxState) { return state['features/base/config']?.securityUi || {}; } + +/** + * Returns the terms, privacy and help centre URL's. + * + * @param {IReduxState} state - The state of the application. + * @returns {{ + * privacy: string, + * helpCentre: string, + * terms: string + * }} + */ +export function getLegalUrls(state: IReduxState) { + const helpCentreURL = state['features/base/config']?.helpCentreURL; + const configLegalUrls = state['features/base/config']?.legalUrls; + + return { + privacy: configLegalUrls?.privacy || DEFAULT_PRIVACY_URL, + helpCentre: helpCentreURL || configLegalUrls?.helpCentre || DEFAULT_HELP_CENTRE_URL, + terms: configLegalUrls?.terms || DEFAULT_TERMS_URL + }; +} diff --git a/react/features/deep-linking/actions.js b/react/features/deep-linking/actions.ts similarity index 98% rename from react/features/deep-linking/actions.js rename to react/features/deep-linking/actions.ts index 54f5f249b3..f686e8270c 100644 --- a/react/features/deep-linking/actions.js +++ b/react/features/deep-linking/actions.ts @@ -1,5 +1,3 @@ -// @flow - import type { Dispatch } from 'redux'; import { appNavigate } from '../app/actions'; diff --git a/react/features/deep-linking/components/DeepLinkingDesktopPage.web.js b/react/features/deep-linking/components/DeepLinkingDesktopPage.web.js deleted file mode 100644 index aba9ab559f..0000000000 --- a/react/features/deep-linking/components/DeepLinkingDesktopPage.web.js +++ /dev/null @@ -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, - - /** - * Used to obtain translations. - */ - t: Function -}; - -/** - * React component representing the deep linking page. - * - * @class DeepLinkingDesktopPage - */ -class DeepLinkingDesktopPage

extends Component

{ - /** - * 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. - -

-
- { - hideLogo - ? null - : { - } -
-
- { - showImage - ?
-
-
-
-
: null - } -
-
-

- { - t(`${_TNS}.title`, - { app: appName }) - } -

-

- { - t( - `${_TNS}.${isSupportedBrowser() - ? 'description' - : 'descriptionWithoutWeb'}`, - { app: appName } - ) - } -

-
-
-
-
-
-
- - ); - } - - _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)); diff --git a/react/features/deep-linking/components/DeepLinkingDesktopPage.web.tsx b/react/features/deep-linking/components/DeepLinkingDesktopPage.web.tsx new file mode 100644 index 0000000000..31d50650cd --- /dev/null +++ b/react/features/deep-linking/components/DeepLinkingDesktopPage.web.tsx @@ -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 = ({ 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 ( +
+
+
+ { + !hideLogo + && { + } +
+
+ { + t(`${_TNS}.titleNew`) + } +
+
{ room }
+
+ { + isSupportedBrowser() + ? translateToHTML(t, `${_TNS}.descriptionNew`, { app: desktop?.appName }) + : t(`${_TNS}.descriptionWithoutWeb`, { app: desktop?.appName }) + } +
+
+
+
+
{translateToHTML(t, 'deepLinking.termsAndConditions', { + termsAndConditionsLink: legalUrls.terms + })} +
+
+
+ ); +}; + +export default translate(DeepLinkingDesktopPage); diff --git a/react/features/deep-linking/components/DeepLinkingMobilePage.web.js b/react/features/deep-linking/components/DeepLinkingMobilePage.web.js deleted file mode 100644 index e4a810238c..0000000000 --- a/react/features/deep-linking/components/DeepLinkingMobilePage.web.js +++ /dev/null @@ -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, - - /** - * The function to translate human-readable text. - */ - t: Function -}; - -/** - * React component representing mobile browser page. - * - * @class DeepLinkingMobilePage - */ -class DeepLinkingMobilePage extends Component { - /** - * 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 ( -
-
- { - hideLogo - ? null - : { - } -
-
-

- { t(`${_TNS}.appNotInstalled`, { app: appName }) } -

-

- { t(`${_TNS}.ifHaveApp`) } -

- - - -

- { t(`${_TNS}.ifDoNotHaveApp`) } -

- - - - { - isSupportedMobileBrowser() - ? ( - - - - ) : ( - - { t(`${_TNS}.unsupportedBrowser`) } - - ) - } - { renderPromotionalFooter() } - -
-
- ); - } - - /** - * 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)); diff --git a/react/features/deep-linking/components/DeepLinkingMobilePage.web.tsx b/react/features/deep-linking/components/DeepLinkingMobilePage.web.tsx new file mode 100644 index 0000000000..5c00bb2b42 --- /dev/null +++ b/react/features/deep-linking/components/DeepLinkingMobilePage.web.tsx @@ -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 = ({ 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 ( +
+
+ {!hideLogo && ({ + )} + +
{ t(`${_TNS}.launchMeetingLabel`) }
+
{room}
+ +
+ ); +}; + +export default translate(DeepLinkingMobilePage); diff --git a/react/features/deep-linking/renderPromotionalFooter.js b/react/features/deep-linking/renderPromotionalFooter.js deleted file mode 100644 index 733de78f4d..0000000000 --- a/react/features/deep-linking/renderPromotionalFooter.js +++ /dev/null @@ -1,9 +0,0 @@ -// @flow -/** - * Method used in order to render a custom promotional footer. - * - * @returns {HTMLElement} - */ -export function renderPromotionalFooter() { - return null; -} diff --git a/react/features/invite/components/dial-in-summary/web/ConferenceID.js b/react/features/invite/components/dial-in-summary/web/ConferenceID.js deleted file mode 100644 index 328957919f..0000000000 --- a/react/features/invite/components/dial-in-summary/web/ConferenceID.js +++ /dev/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 { - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - const { conferenceID, conferenceName, t } = this.props; - - return ( -
-
- { conferenceName } -
-
- { t('info.dialANumber') } -
-
- { `${t('info.dialInConferenceID')} ${_formatConferenceIDPin(conferenceID)}` } -
-
- ); - } -} - -export default translate(ConferenceID); diff --git a/react/features/invite/components/dial-in-summary/web/ConferenceID.tsx b/react/features/invite/components/dial-in-summary/web/ConferenceID.tsx new file mode 100644 index 0000000000..136fddd705 --- /dev/null +++ b/react/features/invite/components/dial-in-summary/web/ConferenceID.tsx @@ -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 = ({ conferenceID, t }) => { + const { classes: styles } = useStyles(); + + return ( +
+
+ To join the meeting via phone, dial one of these numbers and then enter the pin +
+
+
+ { `${t('info.dialInConferenceID')} ${_formatConferenceIDPin(conferenceID ?? '')}` } +
+
+ ); +}; + +export default translate(ConferenceID); diff --git a/react/features/invite/components/dial-in-summary/web/DialInSummary.js b/react/features/invite/components/dial-in-summary/web/DialInSummary.js index 64865af466..5549ace25c 100644 --- a/react/features/invite/components/dial-in-summary/web/DialInSummary.js +++ b/react/features/invite/components/dial-in-summary/web/DialInSummary.js @@ -1,8 +1,13 @@ // @flow + +import { Theme } from '@mui/material'; +import { withStyles } from '@mui/styles'; +import clsx from 'clsx'; import React, { Component } from 'react'; import { translate } from '../../../../base/i18n'; +import { withPixelLineHeight } from '../../../../base/styles/functions.web'; import { getDialInConferenceID, getDialInNumbers } from '../../../_utils'; import ConferenceID from './ConferenceID'; @@ -20,6 +25,11 @@ type Props = { */ className: string, + /** + * An object containing the CSS classes. + */ + classes: any; + /** * Whether or not numbers should include links with the telephone protocol. */ @@ -30,6 +40,16 @@ type Props = { */ room: string, + /** + * Whether the dial in summary container is scrollable. + */ + scrollable: Boolean, + + /** + * Whether the room name should show as title. + */ + showTitle?: boolean, + /** * The url where we were loaded. */ @@ -72,6 +92,26 @@ type State = { numbersEnabled: ?boolean } +const styles = (theme: Theme) => { + return { + hasNumbers: { + alignItems: 'center', + display: 'flex', + flexDirection: 'column', + background: '#1E1E1E', + color: theme.palette.text01 + }, + scrollable: { + height: '100vh', + overflowY: 'scroll' + }, + roomName: { + margin: '40px auto 8px', + ...withPixelLineHeight(theme.typography.heading5) + } + }; +}; + /** * Displays a page listing numbers for dialing into a conference and pin to * the a specific conference. @@ -136,24 +176,27 @@ class DialInSummary extends Component { let contents; const { conferenceID, error, loading, numbersEnabled } = this.state; + const { classes, showTitle, room, clickableNumbers, scrollable, t } = this.props; if (loading) { contents = ''; } else if (numbersEnabled === false) { - contents = this.props.t('info.dialInNotSupported'); + contents = t('info.dialInNotSupported'); } else if (error) { contents = error; } else { - className = 'has-numbers'; + className = clsx(classes.hasNumbers, scrollable && classes.scrollable); contents = [ conferenceID - ? - : null, + ? <> + { showTitle &&
{ room }
} + + : null, @@ -161,7 +204,7 @@ class DialInSummary extends Component { } return ( -
+
{ contents }
); @@ -272,4 +315,4 @@ class DialInSummary extends Component { } } -export default translate(DialInSummary); +export default translate(withStyles(styles)(DialInSummary)); diff --git a/react/features/invite/components/dial-in-summary/web/DialInSummaryApp.js b/react/features/invite/components/dial-in-summary/web/DialInSummaryApp.js new file mode 100644 index 0000000000..2339059de0 --- /dev/null +++ b/react/features/invite/components/dial-in-summary/web/DialInSummaryApp.js @@ -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 + ? + : } + ) + }); + } + + /** + * Overrides the parent method to inject {@link AtlasKitThemeProvider} as + * the top most component. + * + * @override + */ + _createMainElement(component, props) { + return ( + + + + {super._createMainElement(component, props)} + + + ); + } + + /** + * Renders the platform specific dialog container. + * + * @returns {React$Element} + */ + _renderDialogContainer() { + return null; + } +} diff --git a/react/features/invite/components/dial-in-summary/web/NumbersList.js b/react/features/invite/components/dial-in-summary/web/NumbersList.js deleted file mode 100644 index c87e2fc914..0000000000 --- a/react/features/invite/components/dial-in-summary/web/NumbersList.js +++ /dev/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 } | Array, - - /** - * 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 { - /** - * 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} numbersMapping - An object with country - * names as keys and values as arrays of phone numbers. - * @private - * @returns {ReactElement[]} - */ - _renderWithCountries( - numbersMapping: { numbers: Array } | Array) { - 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( - - { this._renderFlag(numbersArray[0].countryCode) } - { countryName } - - { this._renderNumbersList(numbersArray) } - - - { this._renderNumbersTollFreeList(numbersArray) } - - - ); - }); - - return ( - - - - { hasFlags ? - - - - - { rows } - -
: null} - { t('info.country') }{ t('info.numbers') } -
- ); - } - - /** - * 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 ( - - {countryCode === 'SIP' - ? - : - } - ); - } - - 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 => - (
  • - { this._renderNumberLink(number.formattedNumber) } -
  • )); - - return ( -
      - { numbersListItems } -
    - ); - } - - /** - * 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 => - (
  • - { number.tollFree ? t('info.dialInTollFree') : '' } -
  • )); - - return ( -
      - { tollNumbersListItems } -
    - ); - } - - /** - * 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 ( - - { number } - - ); - } - - return number; - } - -} - -export default translate(NumbersList); diff --git a/react/features/invite/components/dial-in-summary/web/NumbersList.tsx b/react/features/invite/components/dial-in-summary/web/NumbersList.tsx new file mode 100644 index 0000000000..9f86e26456 --- /dev/null +++ b/react/features/invite/components/dial-in-summary/web/NumbersList.tsx @@ -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; +} + +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 = ({ t, conferenceID, clickableNumbers, numbers: numbersMapping }) => { + const renderFlag = useCallback((countryCode: string) => { + if (countryCode) { + return ( + + {countryCode === 'SIP' + ? + : + } + ); + } + + 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 ( + + {number} + + ); + } + + return number; + }, [ conferenceID, clickableNumbers ]); + + const renderNumbersList = useCallback((numbers: Array) => { + const numbersListItems = numbers.map(number => + (
  • + {renderNumberLink(number.formattedNumber)} +
  • )); + + return ( +
      + {numbersListItems} +
    + ); + }, []); + + const renderNumbersTollFreeList = useCallback((numbers: Array) => { + const tollNumbersListItems = numbers.map(number => + (
  • + {number.tollFree ? t('info.dialInTollFree') : ''} +
  • )); + + return ( +
      + {tollNumbersListItems} +
    + ); + }, []); + + 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 = numbers[countryName]; + const countryCode = numbersArray[0].countryCode + || countries.getAlpha2Code(countryName, 'en')?.toUpperCase() + || countryName; + + rows.push( + <> + + {renderFlag(countryCode)} + {countryName} + + + + + {renderNumbersList(numbersArray)} + + + {renderNumbersTollFreeList(numbersArray)} + + + + ); + }); + + return rows; + }, [ numbersMapping ]); + + return ( + + + {renderNumbers} + +
    + ); +}; + +export default translate(NumbersList); diff --git a/react/features/settings/components/native/SettingsView.tsx b/react/features/settings/components/native/SettingsView.tsx index b11c3a3998..7e6a01e9c7 100644 --- a/react/features/settings/components/native/SettingsView.tsx +++ b/react/features/settings/components/native/SettingsView.tsx @@ -19,6 +19,7 @@ import { getDefaultURL } from '../../../app/functions.native'; import { IReduxState } from '../../../app/types'; // @ts-ignore import { Avatar } from '../../../base/avatar'; +import { getLegalUrls } from '../../../base/config/functions.native'; import { translate } from '../../../base/i18n/functions'; // @ts-ignore import JitsiScreen from '../../../base/modal/components/JitsiScreen'; @@ -46,19 +47,6 @@ import styles from './styles'; */ const { AppInfo } = NativeModules; -/** - * The URL at which the terms (of service/use) are available to the user. - */ -const TERMS_URL = 'https://jitsi.org/meet/terms'; - -/** - * The URL at which the privacy policy is available to the user. - */ -const PRIVACY_URL = 'https://jitsi.org/meet/privacy'; - - -const DEFAULT_HELP_CENTRE_URL = 'https://web-cdn.jitsi.net/faq/meet-faq.html'; - interface IState { /** @@ -119,11 +107,13 @@ interface IState { interface IProps extends WithTranslation { /** - * The URL for when the help link. - * - * @protected + * The legal URL's. */ - _helpCentreUrl: string; + _legalUrls: { + helpCentre: string; + privacy: string; + terms: string; + }; /** * The ID of the local participant. @@ -710,7 +700,7 @@ class SettingsView extends Component { * @returns {void} */ _onShowHelpPressed() { - Linking.openURL(this.props._helpCentreUrl); + Linking.openURL(this.props._legalUrls.helpCentre); } /** @@ -719,7 +709,7 @@ class SettingsView extends Component { * @returns {void} */ _onShowPrivacyPressed() { - Linking.openURL(PRIVACY_URL); + Linking.openURL(this.props._legalUrls.privacy); } /** @@ -728,7 +718,7 @@ class SettingsView extends Component { * @returns {void} */ _onShowTermsPressed() { - Linking.openURL(TERMS_URL); + Linking.openURL(this.props._legalUrls.terms); } /** @@ -795,7 +785,7 @@ function _mapStateToProps(state: IReduxState) { const localParticipant = getLocalParticipant(state); return { - _helpCentreUrl: state['features/base/config'].helpCentreURL || DEFAULT_HELP_CENTRE_URL, + _legalUrls: getLegalUrls(state), _localParticipantId: localParticipant?.id, _serverURL: getDefaultURL(state), _serverURLChangeEnabled: isServerURLChangeEnabled(state), diff --git a/react/index.web.js b/react/index.web.js index 0efd861754..c3ba869520 100644 --- a/react/index.web.js +++ b/react/index.web.js @@ -7,6 +7,7 @@ import { App } from './features/app/components'; import { getLogger } from './features/base/logging/functions'; import { Platform } from './features/base/react'; import { getJitsiMeetGlobalNS } from './features/base/util'; +import DialInSummaryApp from './features/invite/components/dial-in-summary/web/DialInSummaryApp'; import PrejoinApp from './features/prejoin/components/web/PrejoinApp'; const logger = getLogger('index.web'); @@ -43,7 +44,8 @@ const globalNS = getJitsiMeetGlobalNS(); globalNS.entryPoints = { APP: App, - PREJOIN: PrejoinApp + PREJOIN: PrejoinApp, + DIALIN: DialInSummaryApp }; globalNS.renderEntryPoint = ({ diff --git a/static/dialInInfo.html b/static/dialInInfo.html index 4fa19553b5..a0c08ff2f0 100644 --- a/static/dialInInfo.html +++ b/static/dialInInfo.html @@ -1,17 +1,29 @@ - + - - - - -
    + + - + + + + +
    diff --git a/webpack.config.js b/webpack.config.js index e5ee9aeddc..2938fd4d79 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -322,20 +322,6 @@ module.exports = (_env, argv) => { ], performance: getPerformanceHints(perfHintOptions, 800 * 1024) }), - Object.assign({}, config, { - entry: { - 'dial_in_info_bundle': './react/features/invite/components/dial-in-info-page' - }, - plugins: [ - ...config.plugins, - ...getBundleAnalyzerPlugin(analyzeBundle, 'dial_in_info'), - new webpack.IgnorePlugin({ - resourceRegExp: /^\.\/locale$/, - contextRegExp: /moment$/ - }) - ], - performance: getPerformanceHints(perfHintOptions, 500 * 1024) - }), Object.assign({}, config, { entry: { 'do_external_connect': './connection_optimization/do_external_connect.js'