diff --git a/css/deep-linking/_desktop.scss b/css/deep-linking/_desktop.scss new file mode 100644 index 000000000..9ece9999c --- /dev/null +++ b/css/deep-linking/_desktop.scss @@ -0,0 +1,75 @@ +.deep-linking-desktop { + background-color: #fff; + width: 100%; + height: 100%; + display: flex; + flex-flow: column; + .header { + width: 100%; + height: 55px; + background-color: #f1f2f5; + padding-top: 15px; + padding-left: 50px; + display: flex; + flex-flow: row; + flex: 0 0 55px; + .logo { + height: 40px; + } + } + .content { + padding-top: 40px; + padding-bottom: 40px; + left: 0px; + right: 0px; + display: flex; + width: 100%; + height: 100%; + flex-flow: row; + .leftColumn { + left: 0px; + width: 50%; + min-height: 156px; + display: flex; + flex-flow: column; + .leftColumnContent{ + padding: 20px; + display: flex; + flex-flow: column; + height: 100%; + .image { + background-image: url('../images/deep-linking-image.png'); + background-repeat: no-repeat; + background-position: center; + background-size: contain; + height: 100%; + width: 100%; + } + } + + } + .rightColumn { + top: 0px; + width: 50%; + min-height: 156px; + display: flex; + flex-flow: row; + align-items: center; + .rightColumnContent { + display: flex; + flex-flow: column; + padding: 20px 20px 20px 60px; + .title { + color: #1c2946; + } + .description { + color: #606a80; + margin-top: 8px; + } + .buttons { + margin-top: 16px; + } + } + } + } +} diff --git a/css/deep-linking/_main.scss b/css/deep-linking/_main.scss new file mode 100644 index 000000000..262088ecf --- /dev/null +++ b/css/deep-linking/_main.scss @@ -0,0 +1,3 @@ +@import 'desktop'; +@import 'mobile'; +@import 'no-mobile-app'; diff --git a/css/unsupported-browser/_unsupported-mobile-browser.scss b/css/deep-linking/_mobile.scss similarity index 69% rename from css/unsupported-browser/_unsupported-mobile-browser.scss rename to css/deep-linking/_mobile.scss index 0ab5dbda1..d8ccd4d68 100644 --- a/css/unsupported-browser/_unsupported-mobile-browser.scss +++ b/css/deep-linking/_mobile.scss @@ -1,10 +1,23 @@ -.unsupported-mobile-browser { +.deep-linking-mobile { background-color: #fff; height: 100vh; overflow: auto; position: relative; width: 100vw; + .header { + width: 100%; + height: 70px; + background-color: #f1f2f5; + text-align: center; + .logo { + margin-top: 15px; + margin-left: auto; + margin-right: auto; + height: 40px; + } + } + a { text-decoration: none } @@ -20,10 +33,19 @@ a:active { text-decoration: none; } + + .image { + max-width: 80%; + } + } + + &__text { + font-weight: bolder; + padding: 10px 10px 0px 10px; } &__text, - .unsupported-dial-in { + .deep-linking-dial-in { font-size: 1.2em; line-height: em(29px, 21px); margin-bottom: 0.65em; @@ -39,21 +61,27 @@ } } - &__logo { - height: 108px; - width: 77px; + &__href { + height: 2.2857142857142856em; + line-height: 2.2857142857142856em; + margin: 18px auto 20px; + max-width: 300px; + width: auto; + font-weight: bolder; } &__button { border: 0; height: 2.2857142857142856em; line-height: 2.2857142857142856em; - margin: 18px auto 20px; + margin: 18px auto 10px; + padding: 0px 10px 0px 10px; max-width: 300px; width: auto; @include border-radius(3px); background-color: $unsupportedBrowserButtonBgColor; color: #505F79; + font-weight: bold; &:active { background-color: $unsupportedBrowserButtonBgColor; @@ -69,7 +97,7 @@ } } - .unsupported-dial-in { + .deep-linking-dial-in { display: none; &.has-numbers { diff --git a/css/unsupported-browser/_no-mobile-app.scss b/css/deep-linking/_no-mobile-app.scss similarity index 100% rename from css/unsupported-browser/_no-mobile-app.scss rename to css/deep-linking/_no-mobile-app.scss diff --git a/css/main.scss b/css/main.scss index 4e31c7108..125e34773 100644 --- a/css/main.scss +++ b/css/main.scss @@ -71,5 +71,6 @@ @import 'unsupported-browser/main'; @import 'modals/invite/add-people'; @import 'vertical_filmstrip_overrides'; +@import 'deep-linking/main'; /* Modules END */ diff --git a/css/unsupported-browser/_main.scss b/css/unsupported-browser/_main.scss index 726706763..d40c011d4 100644 --- a/css/unsupported-browser/_main.scss +++ b/css/unsupported-browser/_main.scss @@ -1,3 +1 @@ -@import 'no-mobile-app'; @import 'unsupported-desktop-browser'; -@import 'unsupported-mobile-browser'; diff --git a/images/logo-deep-linking.png b/images/logo-deep-linking.png new file mode 100644 index 000000000..b11108f8b Binary files /dev/null and b/images/logo-deep-linking.png differ diff --git a/interface_config.js b/interface_config.js index 5d52f2317..1cc57ff16 100644 --- a/interface_config.js +++ b/interface_config.js @@ -23,9 +23,11 @@ var interfaceConfig = { SHOW_BRAND_WATERMARK: false, BRAND_WATERMARK_LINK: '', SHOW_POWERED_BY: false, + SHOW_DEEP_LINKING_IMAGE: false, GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true, DISPLAY_WELCOME_PAGE_CONTENT: true, APP_NAME: 'Jitsi Meet', + NATIVE_APP_NAME: 'Jitsi Meet', LANG_DETECTION: false, // Allow i18n to detect the system language INVITATION_POWERED_BY: true, @@ -161,7 +163,7 @@ var interfaceConfig = { /** * Specify mobile app scheme for opening the app from the mobile browser. */ - // MOBILE_APP_SCHEME: 'org.jitsi.meet' + // APP_SCHEME: 'org.jitsi.meet' }; /* eslint-enable no-unused-vars, no-var, max-len */ diff --git a/lang/main.json b/lang/main.json index 9e9892192..168be01bf 100644 --- a/lang/main.json +++ b/lang/main.json @@ -107,11 +107,6 @@ "shortcuts": "View shortcuts", "speakerStats": "Speaker stats" }, - "unsupportedBrowser": { - "appNotInstalled": "Join this meeting with __app__ on your phone.", - "downloadApp": "Download the app", - "openApp": "Continue to __app__" - }, "chat":{ "nickname": { "title": "Enter a nickname in the box below", @@ -561,5 +556,14 @@ }, "sectionList": { "pullToRefresh": "Pull to refresh" + }, + "deepLinking": { + "title": "Launching your meeting in __app__...", + "description": "Nothing happened? We tried launching your meeting in the __app__ desktop app. Try again or launch it in the __app__ web app.", + "tryAgainButton": "Try again in desktop", + "launchWebButton": "Launch in web", + "appNotInstalled": "You need the __app__ mobile app to join this meeting on your phone.", + "downloadApp": "Download the app", + "openApp": "Continue to the app" } } diff --git a/react/features/app/functions.web.js b/react/features/app/functions.web.js index 77979411c..322ae6133 100644 --- a/react/features/app/functions.web.js +++ b/react/features/app/functions.web.js @@ -1,12 +1,10 @@ /* @flow */ -import { Platform } from '../base/react'; import { toState } from '../base/redux'; +import { getDeepLinkingPage } from '../deep-linking'; import { - NoMobileApp, PluginRequiredBrowser, - UnsupportedDesktopBrowser, - UnsupportedMobileBrowser + UnsupportedDesktopBrowser } from '../unsupported-browser'; import { @@ -24,50 +22,18 @@ declare var loggingConfig: Object; * * @private * @param {Object} state - Object containing current redux state. - * @returns {ReactElement|void} + * @returns {Promise|void} * @type {Function[]} */ const _INTERCEPT_COMPONENT_RULES = [ - - /** - * This rule describes case when user opens application using mobile - * browser and is attempting to join a conference. In order to promote the - * app, we choose to suggest the mobile app even if the browser supports the - * app (e.g. Google Chrome with WebRTC support on Android). - * - * @param {Object} state - The redux state of the app. - * @returns {UnsupportedMobileBrowser|void} If the rule is satisfied then - * we should intercept existing component by UnsupportedMobileBrowser. - */ - // eslint-disable-next-line no-unused-vars - state => { - const OS = Platform.OS; - const { room } = state['features/base/conference']; - const isUsingMobileBrowser = OS === 'android' || OS === 'ios'; - - /** - * Checking for presence of a room is done so that interception only - * occurs when trying to enter a meeting but pages outside of meeting, - * like WelcomePage, can still display. - */ - if (room && isUsingMobileBrowser) { - const mobileAppPromo - = typeof interfaceConfig === 'object' - && interfaceConfig.MOBILE_APP_PROMO; - - return ( - typeof mobileAppPromo === 'undefined' || Boolean(mobileAppPromo) - ? UnsupportedMobileBrowser - : NoMobileApp); - } - }, + getDeepLinkingPage, state => { const { webRTCReady } = state['features/base/lib-jitsi-meet']; switch (typeof webRTCReady) { case 'boolean': if (webRTCReady === false) { - return UnsupportedDesktopBrowser; + return Promise.resolve(UnsupportedDesktopBrowser); } break; @@ -76,8 +42,10 @@ const _INTERCEPT_COMPONENT_RULES = [ break; default: - return PluginRequiredBrowser; + return Promise.resolve(PluginRequiredBrowser); } + + return Promise.resolve(); } ]; @@ -87,16 +55,19 @@ const _INTERCEPT_COMPONENT_RULES = [ * * @param {(Object|Function)} stateOrGetState - The redux state or * {@link getState} function. - * @returns {Route} + * @returns {Promise} */ -export function _getRouteToRender(stateOrGetState: Object | Function) { +export function _getRouteToRender(stateOrGetState: Object | Function): Object { const route = _super_getRouteToRender(stateOrGetState); // Intercepts route components if any of component interceptor rules is // satisfied. - route.component = _interceptComponent(stateOrGetState, route.component); + return _interceptComponent(stateOrGetState, route.component).then( + (component: React$Element<*>) => { + route.component = component; - return route; + return route; + }, () => Promise.resolve(route)); } /** @@ -106,23 +77,24 @@ export function _getRouteToRender(stateOrGetState: Object | Function) { * {@link getState} function. * @param {ReactElement} component - Current route component to render. * @private - * @returns {ReactElement} If any of the pre-defined rules is satisfied, returns - * intercepted component. + * @returns {Promise} If any of the pre-defined rules is + * satisfied, returns intercepted component. */ function _interceptComponent( stateOrGetState: Object | Function, component: React$Element<*>) { - let result; const state = toState(stateOrGetState); - for (const rule of _INTERCEPT_COMPONENT_RULES) { - result = rule(state); - if (result) { - break; - } - } + const promises = []; - return result || component; + _INTERCEPT_COMPONENT_RULES.forEach(rule => { + promises.push(rule(state)); + }); + + return Promise.all(promises).then( + results => + results.find(result => typeof result !== 'undefined') || component, + () => Promise.resolve(component)); } /** diff --git a/react/features/app/middleware.js b/react/features/app/middleware.js index 15159352a..a7d0cfd8c 100644 --- a/react/features/app/middleware.js +++ b/react/features/app/middleware.js @@ -77,9 +77,9 @@ function _connectionEstablished(store, next, action) { function _navigate({ getState }) { const state = getState(); const { app } = state['features/app']; - const routeToRender = _getRouteToRender(state); - return app._navigate(routeToRender); + _getRouteToRender(state) + .then(routeToRender => app._navigate(routeToRender)); } /** diff --git a/react/features/unsupported-browser/components/HideNotificationBarStyle.js b/react/features/base/react/components/web/HideNotificationBarStyle.js similarity index 100% rename from react/features/unsupported-browser/components/HideNotificationBarStyle.js rename to react/features/base/react/components/web/HideNotificationBarStyle.js diff --git a/react/features/base/react/components/web/index.js b/react/features/base/react/components/web/index.js index 21916624a..ed07f774d 100644 --- a/react/features/base/react/components/web/index.js +++ b/react/features/base/react/components/web/index.js @@ -1,4 +1,6 @@ export { default as Container } from './Container'; +export { default as HideNotificationBarStyle } + from './HideNotificationBarStyle'; export { default as MultiSelectAutocomplete } from './MultiSelectAutocomplete'; export { default as Text } from './Text'; export { default as Watermarks } from './Watermarks'; diff --git a/react/features/base/util/uri.js b/react/features/base/util/uri.js index b742dd347..855bdd44d 100644 --- a/react/features/base/util/uri.js +++ b/react/features/base/util/uri.js @@ -27,7 +27,7 @@ const _URI_PATH_PATTERN = '([^?#]*)'; * * FIXME: The URL class exposed by JavaScript will not include the colon in * the protocol field. Also in other places (at the time of this writing: - * the UnsupportedMobileBrowser.js) the APP_LINK_SCHEME does not include + * the DeepLinkingMobilePage.js) the APP_LINK_SCHEME does not include * the double dots, so things are inconsistent. * * @type {string} diff --git a/react/features/conference/components/Conference.web.js b/react/features/conference/components/Conference.web.js index 5805a4539..1d8acf611 100644 --- a/react/features/conference/components/Conference.web.js +++ b/react/features/conference/components/Conference.web.js @@ -8,6 +8,7 @@ import { connect, disconnect } from '../../base/connection'; import { DialogContainer } from '../../base/dialog'; import { translate } from '../../base/i18n'; import { CalleeInfoContainer } from '../../base/jwt'; +import { HideNotificationBarStyle } from '../../base/react'; import { Filmstrip } from '../../filmstrip'; import { LargeVideo } from '../../large-video'; import { NotificationsContainer } from '../../notifications'; @@ -18,7 +19,6 @@ import { setToolboxAlwaysVisible, showToolbox } from '../../toolbox'; -import { HideNotificationBarStyle } from '../../unsupported-browser'; import { maybeShowSuboptimalExperienceNotification } from '../functions'; diff --git a/react/features/deep-linking/actionTypes.js b/react/features/deep-linking/actionTypes.js new file mode 100644 index 000000000..ece30cac7 --- /dev/null +++ b/react/features/deep-linking/actionTypes.js @@ -0,0 +1,20 @@ +/* @flow */ + +/** + * The type of the action which signals to open the conference in the desktop + * app. + * + * { + * type: OPEN_DESKTOP + * } + */ +export const OPEN_DESKTOP_APP = Symbol('OPEN_DESKTOP_APP'); + +/** + * The type of the action which signals to open the conference in the web app. + * + * { + * type: OPEN_WEB_APP + * } + */ +export const OPEN_WEB_APP = Symbol('OPEN_WEB_APP'); diff --git a/react/features/deep-linking/actions.js b/react/features/deep-linking/actions.js new file mode 100644 index 000000000..005690b38 --- /dev/null +++ b/react/features/deep-linking/actions.js @@ -0,0 +1,34 @@ +/* @flow */ + +import { appNavigate } from '../app'; + +import { OPEN_DESKTOP_APP, OPEN_WEB_APP } from './actionTypes'; + +/** + * Continue to the conference page. + * + * @returns {Function} + */ +export function openWebApp() { + return (dispatch: Dispatch<*>) => { + // In order to go to the web app we need to skip the deep linking + // interceptor. OPEN_WEB_APP action should set launchInWeb to true in + // the redux store. After this when appNavigate() is called the + // deep linking interceptor will be skipped (will return undefined). + dispatch({ type: OPEN_WEB_APP }); + dispatch(appNavigate()); + }; +} + +/** + * Opens the desktop app. + * + * @returns {{ + * type: OPEN_DESKTOP_APP + * }} + */ +export function openDesktopApp() { + return { + type: OPEN_DESKTOP_APP + }; +} diff --git a/react/features/deep-linking/components/DeepLinkingDesktopPage.js b/react/features/deep-linking/components/DeepLinkingDesktopPage.js new file mode 100644 index 000000000..8dd022d40 --- /dev/null +++ b/react/features/deep-linking/components/DeepLinkingDesktopPage.js @@ -0,0 +1,165 @@ +/* @flow */ + +import Button, { ButtonGroup } from '@atlaskit/button'; +import { AtlasKitThemeProvider } from '@atlaskit/theme'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { translate } from '../../base/i18n'; + +import { + openWebApp, + openDesktopApp +} from '../actions'; +import { _TNS } from '../constants'; + +declare var interfaceConfig: Object; + +/** + * The type of the React {@code Component} props of + * {@link DeepLinkingDesktopPage}. + */ + type Props = { + + /** + * 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._openDesktopApp = this._openDesktopApp.bind(this); + this._onLaunchWeb = this._onLaunchWeb.bind(this); + this._onTryAgain = this._onTryAgain.bind(this); + } + + /** + * Implements the Component's componentDidMount method. + * + * @inheritdoc + */ + componentDidMount() { + this._openDesktopApp(); + } + + /** + * Renders the component. + * + * @returns {ReactElement} + */ + render() { + const { t } = this.props; + const { NATIVE_APP_NAME, SHOW_DEEP_LINKING_IMAGE } = interfaceConfig; + const rightColumnStyle + = SHOW_DEEP_LINKING_IMAGE ? null : { width: '100%' }; + + return ( + + // Enabling light theme because of the color of the buttons. + +

+
+ +
+
+ { + SHOW_DEEP_LINKING_IMAGE + ?
+
+
+
+
: null + } +
+
+

+ { + t(`${_TNS}.title`, + { app: NATIVE_APP_NAME }) + } +

+

+ { + t(`${_TNS}.description`, + { app: NATIVE_APP_NAME }) + } +

+
+ + + + +
+
+
+
+
+ + ); + } + + _openDesktopApp: () => {} + + /** + * Dispatches the openDesktopApp action. + * + * @returns {void} + */ + _openDesktopApp() { + this.props.dispatch(openDesktopApp()); + } + + _onTryAgain: () => {} + + /** + * Handles try again button clicks. + * + * @returns {void} + */ + _onTryAgain() { + this._openDesktopApp(); + } + + _onLaunchWeb: () => {} + + /** + * Handles launch web button clicks. + * + * @returns {void} + */ + _onLaunchWeb() { + this.props.dispatch(openWebApp()); + } +} + +export default translate(connect()(DeepLinkingDesktopPage)); diff --git a/react/features/unsupported-browser/components/UnsupportedMobileBrowser.js b/react/features/deep-linking/components/DeepLinkingMobilePage.js similarity index 54% rename from react/features/unsupported-browser/components/UnsupportedMobileBrowser.js rename to react/features/deep-linking/components/DeepLinkingMobilePage.js index 7d0381a39..ffbea5172 100644 --- a/react/features/unsupported-browser/components/UnsupportedMobileBrowser.js +++ b/react/features/deep-linking/components/DeepLinkingMobilePage.js @@ -5,28 +5,21 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { translate, translateToHTML } from '../../base/i18n'; -import { Platform } from '../../base/react'; -import { URI_PROTOCOL_PATTERN } from '../../base/util'; +import { HideNotificationBarStyle, Platform } from '../../base/react'; import { DialInSummary } from '../../invite'; -import HideNotificationBarStyle from './HideNotificationBarStyle'; + +import { _TNS } from '../constants'; +import { generateDeepLinkingURL } from '../functions'; declare var interfaceConfig: Object; /** - * The namespace of the CSS styles of UnsupportedMobileBrowser. + * The namespace of the CSS styles of DeepLinkingMobilePage. * * @private * @type {string} */ -const _SNS = 'unsupported-mobile-browser'; - -/** - * The namespace of the i18n/translation keys of UnsupportedMobileBrowser. - * - * @private - * @type {string} - */ -const _TNS = 'unsupportedBrowser'; +const _SNS = 'deep-linking-mobile'; /** * The map of platforms to URLs at which the mobile app for the associated @@ -45,13 +38,13 @@ const _URLS = { /** * React component representing mobile browser page. * - * @class UnsupportedMobileBrowser + * @class DeepLinkingMobilePage */ -class UnsupportedMobileBrowser extends Component<*, *> { +class DeepLinkingMobilePage extends Component<*, *> { state: Object; /** - * UnsupportedMobileBrowser component's property types. + * DeepLinkingMobilePage component's property types. * * @static */ @@ -77,20 +70,8 @@ class UnsupportedMobileBrowser extends Component<*, *> { * @inheritdoc */ componentWillMount() { - // If the user installed the app while this Component was displayed - // (e.g. the user clicked the Download the App button), then we would - // like to open the current URL in the mobile app. The only way to do it - // appears to be a link with an app-specific scheme, not a Universal - // Link. - const appScheme = interfaceConfig.MOBILE_APP_SCHEME || 'org.jitsi.meet'; - - // Replace the protocol part with the app scheme. - const joinURL - = window.location.href.replace( - new RegExp(`^${URI_PROTOCOL_PATTERN}`), `${appScheme}:`); - this.setState({ - joinURL + joinURL: generateDeepLinkingURL() }); } @@ -102,43 +83,49 @@ class UnsupportedMobileBrowser extends Component<*, *> { */ render() { const { _room, t } = this.props; - - const openAppButtonClassName + const { NATIVE_APP_NAME, SHOW_DEEP_LINKING_IMAGE } = interfaceConfig; + const downloadButtonClassName = `${_SNS}__button ${_SNS}__button_primary`; - const appName - = interfaceConfig.ADD_PEOPLE_APP_NAME || interfaceConfig.APP_NAME; return (
-
+
+ className = 'logo' + src = '../images/logo-deep-linking.png' /> +
+
+ { + SHOW_DEEP_LINKING_IMAGE + ? + : null + }

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

- - - - - { _room - ? - : null } + + {/* */} + +
@@ -148,7 +135,7 @@ class UnsupportedMobileBrowser extends Component<*, *> { /** * Maps (parts of) the Redux state to the associated props for the - * {@code UnsupportedMobileBrowser} component. + * {@code DeepLinkingMobilePage} component. * * @param {Object} state - The Redux state. * @private @@ -162,4 +149,4 @@ function _mapStateToProps(state) { }; } -export default translate(connect(_mapStateToProps)(UnsupportedMobileBrowser)); +export default translate(connect(_mapStateToProps)(DeepLinkingMobilePage)); diff --git a/react/features/unsupported-browser/components/NoMobileApp.js b/react/features/deep-linking/components/NoMobileApp.js similarity index 80% rename from react/features/unsupported-browser/components/NoMobileApp.js rename to react/features/deep-linking/components/NoMobileApp.js index 029ad81cf..f5e7f223a 100644 --- a/react/features/unsupported-browser/components/NoMobileApp.js +++ b/react/features/deep-linking/components/NoMobileApp.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; -import HideNotificationBarStyle from './HideNotificationBarStyle'; +import { HideNotificationBarStyle } from '../../base/react'; declare var interfaceConfig: Object; @@ -26,8 +26,8 @@ export default class NoMobileApp extends Component<*> { Video chat isn't available on mobile.

- Please use { interfaceConfig.APP_NAME } on desktop to join - calls. + Please use { interfaceConfig.NATIVE_APP_NAME } on desktop to + join calls.

diff --git a/react/features/deep-linking/components/index.js b/react/features/deep-linking/components/index.js new file mode 100644 index 000000000..45fcc430f --- /dev/null +++ b/react/features/deep-linking/components/index.js @@ -0,0 +1,3 @@ +export { default as DeepLinkingDesktopPage } from './DeepLinkingDesktopPage'; +export { default as DeepLinkingMobilePage } from './DeepLinkingMobilePage'; +export { default as NoMobileApp } from './NoMobileApp'; diff --git a/react/features/deep-linking/constants.js b/react/features/deep-linking/constants.js new file mode 100644 index 000000000..b41e8e7f7 --- /dev/null +++ b/react/features/deep-linking/constants.js @@ -0,0 +1,6 @@ +/** + * The namespace of the i18n/translation keys. + * + * @type {string} + */ +export const _TNS = 'deepLinking'; diff --git a/react/features/deep-linking/functions.js b/react/features/deep-linking/functions.js new file mode 100644 index 000000000..5f4f32abd --- /dev/null +++ b/react/features/deep-linking/functions.js @@ -0,0 +1,80 @@ +/* global interfaceConfig */ + +import { URI_PROTOCOL_PATTERN } from '../base/util'; +import { Platform } from '../base/react'; + +import { + DeepLinkingDesktopPage, + DeepLinkingMobilePage, + NoMobileApp +} from './components'; +import { _shouldShowDeepLinkingDesktopPage } + from './shouldShowDeepLinkingDesktopPage'; + +/** + * Generates a deep linking URL based on the current window URL. + * + * @returns {string} - The generated URL. + */ +export function generateDeepLinkingURL() { + // If the user installed the app while this Component was displayed + // (e.g. the user clicked the Download the App button), then we would + // like to open the current URL in the mobile app. The only way to do it + // appears to be a link with an app-specific scheme, not a Universal + // Link. + const appScheme = interfaceConfig.APP_SCHEME || 'org.jitsi.meet'; + + // Replace the protocol part with the app scheme. + + return window.location.href.replace( + new RegExp(`^${URI_PROTOCOL_PATTERN}`), `${appScheme}:`); +} + +/** + * Resolves with the component that should be displayed if the deep linking page + * should be shown and with undefined otherwise. + * + * @param {Object} state - Object containing current redux state. + * @returns {Promise} + */ +export function getDeepLinkingPage(state) { + const { room } = state['features/base/conference']; + + // Show only if we are about to join a conference. + if (!room) { + return Promise.resolve(); + } + + const OS = Platform.OS; + const isUsingMobileBrowser = OS === 'android' || OS === 'ios'; + + if (isUsingMobileBrowser) { // mobile + const mobileAppPromo + = typeof interfaceConfig === 'object' + && interfaceConfig.MOBILE_APP_PROMO; + + return Promise.resolve( + typeof mobileAppPromo === 'undefined' || Boolean(mobileAppPromo) + ? DeepLinkingMobilePage : NoMobileApp); + } + + // desktop + const { launchInWeb } = state['features/deep-linking']; + + if (launchInWeb) { + return Promise.resolve(); + } + + return _shouldShowDeepLinkingDesktopPage().then( + // eslint-disable-next-line no-confusing-arrow + show => show ? DeepLinkingDesktopPage : undefined); +} + +/** + * Opens the desktop app. + * + * @returns {void} + */ +export function openDesktopApp() { + window.location.href = generateDeepLinkingURL(); +} diff --git a/react/features/deep-linking/index.js b/react/features/deep-linking/index.js new file mode 100644 index 000000000..c19d5eb7b --- /dev/null +++ b/react/features/deep-linking/index.js @@ -0,0 +1,4 @@ +export * from './functions'; + +import './middleware'; +import './reducer'; diff --git a/react/features/deep-linking/middleware.js b/react/features/deep-linking/middleware.js new file mode 100644 index 000000000..6facca567 --- /dev/null +++ b/react/features/deep-linking/middleware.js @@ -0,0 +1,23 @@ +// @flow + +import { MiddlewareRegistry } from '../base/redux'; + +import { OPEN_DESKTOP_APP } from './actionTypes'; +import { openDesktopApp } from './functions'; + +/** + * Implements the middleware of the deep linking feature. + * + * @param {Store} store - The redux store. + * @returns {Function} + */ +// eslint-disable-next-line no-unused-vars +MiddlewareRegistry.register(store => next => action => { + switch (action.type) { + case OPEN_DESKTOP_APP: + openDesktopApp(); + break; + } + + return next(action); +}); diff --git a/react/features/deep-linking/reducer.js b/react/features/deep-linking/reducer.js new file mode 100644 index 000000000..2ef34a041 --- /dev/null +++ b/react/features/deep-linking/reducer.js @@ -0,0 +1,18 @@ +/* @flow */ + +import { ReducerRegistry } from '../base/redux'; + +import { OPEN_WEB_APP } from './actionTypes'; + +ReducerRegistry.register('features/deep-linking', (state = {}, action) => { + switch (action.type) { + case OPEN_WEB_APP: { + return { + ...state, + launchInWeb: true + }; + } + } + + return state; +}); diff --git a/react/features/deep-linking/shouldShowDeepLinkingDesktopPage.js b/react/features/deep-linking/shouldShowDeepLinkingDesktopPage.js new file mode 100644 index 000000000..2f04305e5 --- /dev/null +++ b/react/features/deep-linking/shouldShowDeepLinkingDesktopPage.js @@ -0,0 +1,9 @@ +/** + * Resolves with true if the deep linking page should be shown and with + * false otherwise. + * + * @returns {Promise} + */ +export function _shouldShowDeepLinkingDesktopPage() { + return Promise.resolve(false); +} diff --git a/react/features/invite/components/dial-in-summary/DialInSummary.web.js b/react/features/invite/components/dial-in-summary/DialInSummary.web.js index 95bd9cae0..4cb565496 100644 --- a/react/features/invite/components/dial-in-summary/DialInSummary.web.js +++ b/react/features/invite/components/dial-in-summary/DialInSummary.web.js @@ -129,6 +129,7 @@ class DialInSummary extends Component { : null, ]; diff --git a/react/features/invite/components/dial-in-summary/NumbersList.web.js b/react/features/invite/components/dial-in-summary/NumbersList.web.js index 490aae7a3..f8556c316 100644 --- a/react/features/invite/components/dial-in-summary/NumbersList.web.js +++ b/react/features/invite/components/dial-in-summary/NumbersList.web.js @@ -21,6 +21,11 @@ class NumbersList extends Component { */ clickableNumbers: PropTypes.bool, + /** + * The conference ID for dialing in. + */ + conferenceID: PropTypes.number, + /** * The phone numbers to display. Can be an array of numbers * or an object with countries as keys and an array of numbers @@ -136,7 +141,7 @@ class NumbersList extends Component { if (this.props.clickableNumbers) { return ( { number } diff --git a/react/features/unsupported-browser/actionTypes.js b/react/features/unsupported-browser/actionTypes.js deleted file mode 100644 index 95d9af9a8..000000000 --- a/react/features/unsupported-browser/actionTypes.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * The type of the Redux action which signals that the React Component - * UnsupportedMobileBrowser which was rendered as a promotion of the mobile app - * on a browser was dismissed by the user. For example, the Web app may possibly - * run in Google Chrome on Android but we choose to promote the mobile app - * anyway claiming the user experience provided by the Web app is inferior to - * that of the mobile app. Eventually, the user may choose to dismiss the - * promotion of the mobile app and take their chances with the Web app instead. - * If unused, then we have chosen to force the mobile app and not allow the Web - * app in mobile browsers. - * - * { - * type: DISMISS_MOBILE_APP_PROMO - * } - */ -export const DISMISS_MOBILE_APP_PROMO = Symbol('DISMISS_MOBILE_APP_PROMO'); diff --git a/react/features/unsupported-browser/actions.js b/react/features/unsupported-browser/actions.js deleted file mode 100644 index c07c8a7d5..000000000 --- a/react/features/unsupported-browser/actions.js +++ /dev/null @@ -1,21 +0,0 @@ -import { DISMISS_MOBILE_APP_PROMO } from './actionTypes'; - -/** - * Returns a Redux action which signals that the UnsupportedMobileBrowser which - * was rendered as a promotion of the mobile app on a browser was dismissed by - * the user. For example, the Web app may possibly run in Google Chrome - * on Android but we choose to promote the mobile app anyway claiming the user - * experience provided by the Web app is inferior to that of the mobile app. - * Eventually, the user may choose to dismiss the promotion of the mobile app - * and take their chances with the Web app instead. If unused, then we have - * chosen to force the mobile app and not allow the Web app in mobile browsers. - * - * @returns {{ - * type: DISMISS_MOBILE_APP_PROMO - * }} - */ -export function dismissMobileAppPromo() { - return { - type: DISMISS_MOBILE_APP_PROMO - }; -} diff --git a/react/features/unsupported-browser/components/UnsupportedDesktopBrowser.js b/react/features/unsupported-browser/components/UnsupportedDesktopBrowser.js index 8473bc6e4..e395106c1 100644 --- a/react/features/unsupported-browser/components/UnsupportedDesktopBrowser.js +++ b/react/features/unsupported-browser/components/UnsupportedDesktopBrowser.js @@ -4,10 +4,9 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { translate } from '../../base/i18n'; -import { Platform } from '../../base/react'; +import { HideNotificationBarStyle, Platform } from '../../base/react'; import { CHROME, FIREFOX, IE, SAFARI } from './browserLinks'; -import HideNotificationBarStyle from './HideNotificationBarStyle'; /** * The namespace of the CSS styles of UnsupportedDesktopBrowser. diff --git a/react/features/unsupported-browser/components/index.js b/react/features/unsupported-browser/components/index.js index 34be115bf..142fbf841 100644 --- a/react/features/unsupported-browser/components/index.js +++ b/react/features/unsupported-browser/components/index.js @@ -1,8 +1,3 @@ -export { default as HideNotificationBarStyle } - from './HideNotificationBarStyle'; -export { default as NoMobileApp } from './NoMobileApp'; export { default as PluginRequiredBrowser } from './PluginRequiredBrowser'; export { default as UnsupportedDesktopBrowser } from './UnsupportedDesktopBrowser'; -export { default as UnsupportedMobileBrowser } - from './UnsupportedMobileBrowser'; diff --git a/react/features/unsupported-browser/index.js b/react/features/unsupported-browser/index.js index 7f0ef0251..7ce19e0e6 100644 --- a/react/features/unsupported-browser/index.js +++ b/react/features/unsupported-browser/index.js @@ -1,5 +1,3 @@ -export * from './actions'; export * from './components'; import './middleware'; -import './reducer'; diff --git a/react/features/unsupported-browser/reducer.js b/react/features/unsupported-browser/reducer.js deleted file mode 100644 index 72fc5393a..000000000 --- a/react/features/unsupported-browser/reducer.js +++ /dev/null @@ -1,27 +0,0 @@ -import { ReducerRegistry } from '../base/redux'; - -import { DISMISS_MOBILE_APP_PROMO } from './actionTypes'; - -ReducerRegistry.register( - 'features/unsupported-browser', - (state = {}, action) => { - switch (action.type) { - case DISMISS_MOBILE_APP_PROMO: - return { - ...state, - - /** - * The indicator which determines whether the React - * Component UnsupportedMobileBrowser which was rendered as - * a promotion of the mobile app on a browser was dismissed - * by the user. If unused, then we have chosen to force the - * mobile app and not allow the Web app in mobile browsers. - * - * @type {boolean} - */ - mobileAppPromoDismissed: true - }; - } - - return state; - }); diff --git a/react/features/welcome/components/WelcomePage.web.js b/react/features/welcome/components/WelcomePage.web.js index aeb3c981d..a84576c91 100644 --- a/react/features/welcome/components/WelcomePage.web.js +++ b/react/features/welcome/components/WelcomePage.web.js @@ -9,8 +9,7 @@ import { connect } from 'react-redux'; import { initAnalytics } from '../../analytics'; import { translate } from '../../base/i18n'; import { isAnalyticsEnabled } from '../../base/lib-jitsi-meet'; -import { Watermarks } from '../../base/react'; -import { HideNotificationBarStyle } from '../../unsupported-browser'; +import { HideNotificationBarStyle, Watermarks } from '../../base/react'; import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';