Deeplinking (#2760)

* feat(Deeplinking): Implement for web.

* ref(unsupported_browser): Move the mobile version to deeplinking feature

* feat(deeplinking_mobile): Redesign.

* fix(deeplinking): Use interface.NATIVE_APP_NAME.

* feat(dial_in_summary): Add the PIN to the number link.

* fix(deep_linking): Handle use case when there isn't deep linking image.

* fix(deep_linking): css

* fix(deep_linking): deeplink -> "deep linking"

* fix(deeplinking_css): Remove position: fixed

* docs(deeplinking): Add comment for the openWebApp action.
This commit is contained in:
hristoterezov 2018-04-13 19:00:40 -05:00 committed by virtuacoplenny
parent fd44721bac
commit eb19f94598
36 changed files with 571 additions and 204 deletions

View File

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

View File

@ -0,0 +1,3 @@
@import 'desktop';
@import 'mobile';
@import 'no-mobile-app';

View File

@ -1,10 +1,23 @@
.unsupported-mobile-browser { .deep-linking-mobile {
background-color: #fff; background-color: #fff;
height: 100vh; height: 100vh;
overflow: auto; overflow: auto;
position: relative; position: relative;
width: 100vw; 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 { a {
text-decoration: none text-decoration: none
} }
@ -20,10 +33,19 @@
a:active { a:active {
text-decoration: none; text-decoration: none;
} }
.image {
max-width: 80%;
}
}
&__text {
font-weight: bolder;
padding: 10px 10px 0px 10px;
} }
&__text, &__text,
.unsupported-dial-in { .deep-linking-dial-in {
font-size: 1.2em; font-size: 1.2em;
line-height: em(29px, 21px); line-height: em(29px, 21px);
margin-bottom: 0.65em; margin-bottom: 0.65em;
@ -39,21 +61,27 @@
} }
} }
&__logo { &__href {
height: 108px; height: 2.2857142857142856em;
width: 77px; line-height: 2.2857142857142856em;
margin: 18px auto 20px;
max-width: 300px;
width: auto;
font-weight: bolder;
} }
&__button { &__button {
border: 0; border: 0;
height: 2.2857142857142856em; height: 2.2857142857142856em;
line-height: 2.2857142857142856em; line-height: 2.2857142857142856em;
margin: 18px auto 20px; margin: 18px auto 10px;
padding: 0px 10px 0px 10px;
max-width: 300px; max-width: 300px;
width: auto; width: auto;
@include border-radius(3px); @include border-radius(3px);
background-color: $unsupportedBrowserButtonBgColor; background-color: $unsupportedBrowserButtonBgColor;
color: #505F79; color: #505F79;
font-weight: bold;
&:active { &:active {
background-color: $unsupportedBrowserButtonBgColor; background-color: $unsupportedBrowserButtonBgColor;
@ -69,7 +97,7 @@
} }
} }
.unsupported-dial-in { .deep-linking-dial-in {
display: none; display: none;
&.has-numbers { &.has-numbers {

View File

@ -71,5 +71,6 @@
@import 'unsupported-browser/main'; @import 'unsupported-browser/main';
@import 'modals/invite/add-people'; @import 'modals/invite/add-people';
@import 'vertical_filmstrip_overrides'; @import 'vertical_filmstrip_overrides';
@import 'deep-linking/main';
/* Modules END */ /* Modules END */

View File

@ -1,3 +1 @@
@import 'no-mobile-app';
@import 'unsupported-desktop-browser'; @import 'unsupported-desktop-browser';
@import 'unsupported-mobile-browser';

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -23,9 +23,11 @@ var interfaceConfig = {
SHOW_BRAND_WATERMARK: false, SHOW_BRAND_WATERMARK: false,
BRAND_WATERMARK_LINK: '', BRAND_WATERMARK_LINK: '',
SHOW_POWERED_BY: false, SHOW_POWERED_BY: false,
SHOW_DEEP_LINKING_IMAGE: false,
GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true, GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true,
DISPLAY_WELCOME_PAGE_CONTENT: true, DISPLAY_WELCOME_PAGE_CONTENT: true,
APP_NAME: 'Jitsi Meet', APP_NAME: 'Jitsi Meet',
NATIVE_APP_NAME: 'Jitsi Meet',
LANG_DETECTION: false, // Allow i18n to detect the system language LANG_DETECTION: false, // Allow i18n to detect the system language
INVITATION_POWERED_BY: true, INVITATION_POWERED_BY: true,
@ -161,7 +163,7 @@ var interfaceConfig = {
/** /**
* Specify mobile app scheme for opening the app from the mobile browser. * 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 */ /* eslint-enable no-unused-vars, no-var, max-len */

View File

@ -107,11 +107,6 @@
"shortcuts": "View shortcuts", "shortcuts": "View shortcuts",
"speakerStats": "Speaker stats" "speakerStats": "Speaker stats"
}, },
"unsupportedBrowser": {
"appNotInstalled": "Join this meeting with __app__ on your phone.",
"downloadApp": "Download the app",
"openApp": "Continue to __app__"
},
"chat":{ "chat":{
"nickname": { "nickname": {
"title": "Enter a nickname in the box below", "title": "Enter a nickname in the box below",
@ -561,5 +556,14 @@
}, },
"sectionList": { "sectionList": {
"pullToRefresh": "Pull to refresh" "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"
} }
} }

View File

@ -1,12 +1,10 @@
/* @flow */ /* @flow */
import { Platform } from '../base/react';
import { toState } from '../base/redux'; import { toState } from '../base/redux';
import { getDeepLinkingPage } from '../deep-linking';
import { import {
NoMobileApp,
PluginRequiredBrowser, PluginRequiredBrowser,
UnsupportedDesktopBrowser, UnsupportedDesktopBrowser
UnsupportedMobileBrowser
} from '../unsupported-browser'; } from '../unsupported-browser';
import { import {
@ -24,50 +22,18 @@ declare var loggingConfig: Object;
* *
* @private * @private
* @param {Object} state - Object containing current redux state. * @param {Object} state - Object containing current redux state.
* @returns {ReactElement|void} * @returns {Promise<ReactElement>|void}
* @type {Function[]} * @type {Function[]}
*/ */
const _INTERCEPT_COMPONENT_RULES = [ const _INTERCEPT_COMPONENT_RULES = [
getDeepLinkingPage,
/**
* 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);
}
},
state => { state => {
const { webRTCReady } = state['features/base/lib-jitsi-meet']; const { webRTCReady } = state['features/base/lib-jitsi-meet'];
switch (typeof webRTCReady) { switch (typeof webRTCReady) {
case 'boolean': case 'boolean':
if (webRTCReady === false) { if (webRTCReady === false) {
return UnsupportedDesktopBrowser; return Promise.resolve(UnsupportedDesktopBrowser);
} }
break; break;
@ -76,8 +42,10 @@ const _INTERCEPT_COMPONENT_RULES = [
break; break;
default: 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 * @param {(Object|Function)} stateOrGetState - The redux state or
* {@link getState} function. * {@link getState} function.
* @returns {Route} * @returns {Promise<Route>}
*/ */
export function _getRouteToRender(stateOrGetState: Object | Function) { export function _getRouteToRender(stateOrGetState: Object | Function): Object {
const route = _super_getRouteToRender(stateOrGetState); const route = _super_getRouteToRender(stateOrGetState);
// Intercepts route components if any of component interceptor rules is // Intercepts route components if any of component interceptor rules is
// satisfied. // 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. * {@link getState} function.
* @param {ReactElement} component - Current route component to render. * @param {ReactElement} component - Current route component to render.
* @private * @private
* @returns {ReactElement} If any of the pre-defined rules is satisfied, returns * @returns {Promise<ReactElement>} If any of the pre-defined rules is
* intercepted component. * satisfied, returns intercepted component.
*/ */
function _interceptComponent( function _interceptComponent(
stateOrGetState: Object | Function, stateOrGetState: Object | Function,
component: React$Element<*>) { component: React$Element<*>) {
let result;
const state = toState(stateOrGetState); const state = toState(stateOrGetState);
for (const rule of _INTERCEPT_COMPONENT_RULES) { const promises = [];
result = rule(state);
if (result) {
break;
}
}
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));
} }
/** /**

View File

@ -77,9 +77,9 @@ function _connectionEstablished(store, next, action) {
function _navigate({ getState }) { function _navigate({ getState }) {
const state = getState(); const state = getState();
const { app } = state['features/app']; const { app } = state['features/app'];
const routeToRender = _getRouteToRender(state);
return app._navigate(routeToRender); _getRouteToRender(state)
.then(routeToRender => app._navigate(routeToRender));
} }
/** /**

View File

@ -1,4 +1,6 @@
export { default as Container } from './Container'; export { default as Container } from './Container';
export { default as HideNotificationBarStyle }
from './HideNotificationBarStyle';
export { default as MultiSelectAutocomplete } from './MultiSelectAutocomplete'; export { default as MultiSelectAutocomplete } from './MultiSelectAutocomplete';
export { default as Text } from './Text'; export { default as Text } from './Text';
export { default as Watermarks } from './Watermarks'; export { default as Watermarks } from './Watermarks';

View File

@ -27,7 +27,7 @@ const _URI_PATH_PATTERN = '([^?#]*)';
* *
* FIXME: The URL class exposed by JavaScript will not include the colon in * 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 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. * the double dots, so things are inconsistent.
* *
* @type {string} * @type {string}

View File

@ -8,6 +8,7 @@ import { connect, disconnect } from '../../base/connection';
import { DialogContainer } from '../../base/dialog'; import { DialogContainer } from '../../base/dialog';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { CalleeInfoContainer } from '../../base/jwt'; import { CalleeInfoContainer } from '../../base/jwt';
import { HideNotificationBarStyle } from '../../base/react';
import { Filmstrip } from '../../filmstrip'; import { Filmstrip } from '../../filmstrip';
import { LargeVideo } from '../../large-video'; import { LargeVideo } from '../../large-video';
import { NotificationsContainer } from '../../notifications'; import { NotificationsContainer } from '../../notifications';
@ -18,7 +19,6 @@ import {
setToolboxAlwaysVisible, setToolboxAlwaysVisible,
showToolbox showToolbox
} from '../../toolbox'; } from '../../toolbox';
import { HideNotificationBarStyle } from '../../unsupported-browser';
import { maybeShowSuboptimalExperienceNotification } from '../functions'; import { maybeShowSuboptimalExperienceNotification } from '../functions';

View File

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

View File

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

View File

@ -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<P : Props> extends Component<P> {
/**
* Initializes a new {@code DeepLinkingDesktopPage} instance.
*
* @param {Object} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props: P) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._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.
<AtlasKitThemeProvider mode = 'light'>
<div className = 'deep-linking-desktop'>
<div className = 'header'>
<img
className = 'logo'
src = '../images/logo-deep-linking.png' />
</div>
<div className = 'content'>
{
SHOW_DEEP_LINKING_IMAGE
? <div className = 'leftColumn'>
<div className = 'leftColumnContent'>
<div className = 'image' />
</div>
</div> : null
}
<div
className = 'rightColumn'
style = { rightColumnStyle }>
<div className = 'rightColumnContent'>
<h1 className = 'title'>
{
t(`${_TNS}.title`,
{ app: NATIVE_APP_NAME })
}
</h1>
<p className = 'description'>
{
t(`${_TNS}.description`,
{ app: NATIVE_APP_NAME })
}
</p>
<div className = 'buttons'>
<ButtonGroup>
<Button
appearance = 'default'
onClick = { this._onTryAgain }>
{ t(`${_TNS}.tryAgainButton`) }
</Button>
<Button onClick = { this._onLaunchWeb }>
{ t(`${_TNS}.launchWebButton`) }
</Button>
</ButtonGroup>
</div>
</div>
</div>
</div>
</div>
</AtlasKitThemeProvider>
);
}
_openDesktopApp: () => {}
/**
* Dispatches the <tt>openDesktopApp</tt> 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));

View File

@ -5,28 +5,21 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { translate, translateToHTML } from '../../base/i18n'; import { translate, translateToHTML } from '../../base/i18n';
import { Platform } from '../../base/react'; import { HideNotificationBarStyle, Platform } from '../../base/react';
import { URI_PROTOCOL_PATTERN } from '../../base/util';
import { DialInSummary } from '../../invite'; import { DialInSummary } from '../../invite';
import HideNotificationBarStyle from './HideNotificationBarStyle';
import { _TNS } from '../constants';
import { generateDeepLinkingURL } from '../functions';
declare var interfaceConfig: Object; declare var interfaceConfig: Object;
/** /**
* The namespace of the CSS styles of UnsupportedMobileBrowser. * The namespace of the CSS styles of DeepLinkingMobilePage.
* *
* @private * @private
* @type {string} * @type {string}
*/ */
const _SNS = 'unsupported-mobile-browser'; const _SNS = 'deep-linking-mobile';
/**
* The namespace of the i18n/translation keys of UnsupportedMobileBrowser.
*
* @private
* @type {string}
*/
const _TNS = 'unsupportedBrowser';
/** /**
* The map of platforms to URLs at which the mobile app for the associated * 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. * React component representing mobile browser page.
* *
* @class UnsupportedMobileBrowser * @class DeepLinkingMobilePage
*/ */
class UnsupportedMobileBrowser extends Component<*, *> { class DeepLinkingMobilePage extends Component<*, *> {
state: Object; state: Object;
/** /**
* UnsupportedMobileBrowser component's property types. * DeepLinkingMobilePage component's property types.
* *
* @static * @static
*/ */
@ -77,20 +70,8 @@ class UnsupportedMobileBrowser extends Component<*, *> {
* @inheritdoc * @inheritdoc
*/ */
componentWillMount() { 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({ this.setState({
joinURL joinURL: generateDeepLinkingURL()
}); });
} }
@ -102,43 +83,49 @@ class UnsupportedMobileBrowser extends Component<*, *> {
*/ */
render() { render() {
const { _room, t } = this.props; const { _room, t } = this.props;
const { NATIVE_APP_NAME, SHOW_DEEP_LINKING_IMAGE } = interfaceConfig;
const openAppButtonClassName const downloadButtonClassName
= `${_SNS}__button ${_SNS}__button_primary`; = `${_SNS}__button ${_SNS}__button_primary`;
const appName
= interfaceConfig.ADD_PEOPLE_APP_NAME || interfaceConfig.APP_NAME;
return ( return (
<div className = { _SNS }> <div className = { _SNS }>
<div className = { `${_SNS}__body` }> <div className = 'header'>
<img <img
className = { `${_SNS}__logo` } className = 'logo'
src = 'images/logo-blue.svg' /> src = '../images/logo-deep-linking.png' />
</div>
<div className = { `${_SNS}__body` }>
{
SHOW_DEEP_LINKING_IMAGE
? <img
className = 'image'
src = '../images/deep-linking-image.png' />
: null
}
<p className = { `${_SNS}__text` }> <p className = { `${_SNS}__text` }>
{ {
translateToHTML( translateToHTML(
t, t,
`${_TNS}.appNotInstalled`, `${_TNS}.appNotInstalled`,
{ app: appName }) { app: NATIVE_APP_NAME })
} }
</p> </p>
<a href = { this.state.joinURL }>
<button className = { openAppButtonClassName }>
{ t(`${_TNS}.openApp`,
{ app: appName }) }
</button>
</a>
<a href = { _URLS[Platform.OS] }> <a href = { _URLS[Platform.OS] }>
<button className = { `${_SNS}__button` }> <button className = { downloadButtonClassName }>
{ t(`${_TNS}.downloadApp`) } { t(`${_TNS}.downloadApp`) }
</button> </button>
</a> </a>
{ _room <a
? <DialInSummary className = { `${_SNS}__href` }
className = 'unsupported-dial-in' href = { this.state.joinURL }>
clickableNumbers = { true } {/* <button className = { `${_SNS}__button` }> */}
room = { _room } /> { t(`${_TNS}.openApp`) }
: null } {/* </button> */}
</a>
<DialInSummary
className = 'deep-linking-dial-in'
clickableNumbers = { true }
room = { _room } />
</div> </div>
<HideNotificationBarStyle /> <HideNotificationBarStyle />
</div> </div>
@ -148,7 +135,7 @@ class UnsupportedMobileBrowser extends Component<*, *> {
/** /**
* Maps (parts of) the Redux state to the associated props for the * Maps (parts of) the Redux state to the associated props for the
* {@code UnsupportedMobileBrowser} component. * {@code DeepLinkingMobilePage} component.
* *
* @param {Object} state - The Redux state. * @param {Object} state - The Redux state.
* @private * @private
@ -162,4 +149,4 @@ function _mapStateToProps(state) {
}; };
} }
export default translate(connect(_mapStateToProps)(UnsupportedMobileBrowser)); export default translate(connect(_mapStateToProps)(DeepLinkingMobilePage));

View File

@ -2,7 +2,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import HideNotificationBarStyle from './HideNotificationBarStyle'; import { HideNotificationBarStyle } from '../../base/react';
declare var interfaceConfig: Object; declare var interfaceConfig: Object;
@ -26,8 +26,8 @@ export default class NoMobileApp extends Component<*> {
Video chat isn't available on mobile. Video chat isn't available on mobile.
</h2> </h2>
<p className = { `${ns}__description` }> <p className = { `${ns}__description` }>
Please use { interfaceConfig.APP_NAME } on desktop to join Please use { interfaceConfig.NATIVE_APP_NAME } on desktop to
calls. join calls.
</p> </p>
<HideNotificationBarStyle /> <HideNotificationBarStyle />

View File

@ -0,0 +1,3 @@
export { default as DeepLinkingDesktopPage } from './DeepLinkingDesktopPage';
export { default as DeepLinkingMobilePage } from './DeepLinkingMobilePage';
export { default as NoMobileApp } from './NoMobileApp';

View File

@ -0,0 +1,6 @@
/**
* The namespace of the i18n/translation keys.
*
* @type {string}
*/
export const _TNS = 'deepLinking';

View File

@ -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 <tt>undefined</tt> otherwise.
*
* @param {Object} state - Object containing current redux state.
* @returns {Promise<Component>}
*/
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();
}

View File

@ -0,0 +1,4 @@
export * from './functions';
import './middleware';
import './reducer';

View File

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

View File

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

View File

@ -0,0 +1,9 @@
/**
* Resolves with <tt>true</tt> if the deep linking page should be shown and with
* <tt>false</tt> otherwise.
*
* @returns {Promise<boolean>}
*/
export function _shouldShowDeepLinkingDesktopPage() {
return Promise.resolve(false);
}

View File

@ -129,6 +129,7 @@ class DialInSummary extends Component {
: null, : null,
<NumbersList <NumbersList
clickableNumbers = { this.props.clickableNumbers } clickableNumbers = { this.props.clickableNumbers }
conferenceID = { conferenceID }
key = 'numbers' key = 'numbers'
numbers = { this.state.numbers } /> numbers = { this.state.numbers } />
]; ];

View File

@ -21,6 +21,11 @@ class NumbersList extends Component {
*/ */
clickableNumbers: PropTypes.bool, clickableNumbers: PropTypes.bool,
/**
* The conference ID for dialing in.
*/
conferenceID: PropTypes.number,
/** /**
* The phone numbers to display. Can be an array of numbers * The phone numbers to display. Can be an array of numbers
* or an object with countries as keys and 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) { if (this.props.clickableNumbers) {
return ( return (
<a <a
href = { `tel:${number}` } href = { `tel:${number}p${this.props.conferenceID}#` }
key = { number } > key = { number } >
{ number } { number }
</a> </a>

View File

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

View File

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

View File

@ -4,10 +4,9 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { Platform } from '../../base/react'; import { HideNotificationBarStyle, Platform } from '../../base/react';
import { CHROME, FIREFOX, IE, SAFARI } from './browserLinks'; import { CHROME, FIREFOX, IE, SAFARI } from './browserLinks';
import HideNotificationBarStyle from './HideNotificationBarStyle';
/** /**
* The namespace of the CSS styles of UnsupportedDesktopBrowser. * The namespace of the CSS styles of UnsupportedDesktopBrowser.

View File

@ -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 PluginRequiredBrowser } from './PluginRequiredBrowser';
export { default as UnsupportedDesktopBrowser } export { default as UnsupportedDesktopBrowser }
from './UnsupportedDesktopBrowser'; from './UnsupportedDesktopBrowser';
export { default as UnsupportedMobileBrowser }
from './UnsupportedMobileBrowser';

View File

@ -1,5 +1,3 @@
export * from './actions';
export * from './components'; export * from './components';
import './middleware'; import './middleware';
import './reducer';

View File

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

View File

@ -9,8 +9,7 @@ import { connect } from 'react-redux';
import { initAnalytics } from '../../analytics'; import { initAnalytics } from '../../analytics';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { isAnalyticsEnabled } from '../../base/lib-jitsi-meet'; import { isAnalyticsEnabled } from '../../base/lib-jitsi-meet';
import { Watermarks } from '../../base/react'; import { HideNotificationBarStyle, Watermarks } from '../../base/react';
import { HideNotificationBarStyle } from '../../unsupported-browser';
import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage'; import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';