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:
parent
fd44721bac
commit
eb19f94598
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
@import 'desktop';
|
||||
@import 'mobile';
|
||||
@import 'no-mobile-app';
|
|
@ -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 {
|
|
@ -71,5 +71,6 @@
|
|||
@import 'unsupported-browser/main';
|
||||
@import 'modals/invite/add-people';
|
||||
@import 'vertical_filmstrip_overrides';
|
||||
@import 'deep-linking/main';
|
||||
|
||||
/* Modules END */
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
@import 'no-mobile-app';
|
||||
@import 'unsupported-desktop-browser';
|
||||
@import 'unsupported-mobile-browser';
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
|
@ -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 */
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ReactElement>|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<Route>}
|
||||
*/
|
||||
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;
|
||||
}, () => 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<ReactElement>} 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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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');
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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));
|
|
@ -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 (
|
||||
<div className = { _SNS }>
|
||||
<div className = { `${_SNS}__body` }>
|
||||
<div className = 'header'>
|
||||
<img
|
||||
className = { `${_SNS}__logo` }
|
||||
src = 'images/logo-blue.svg' />
|
||||
className = 'logo'
|
||||
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` }>
|
||||
{
|
||||
translateToHTML(
|
||||
t,
|
||||
`${_TNS}.appNotInstalled`,
|
||||
{ app: appName })
|
||||
{ app: NATIVE_APP_NAME })
|
||||
}
|
||||
</p>
|
||||
<a href = { this.state.joinURL }>
|
||||
<button className = { openAppButtonClassName }>
|
||||
{ t(`${_TNS}.openApp`,
|
||||
{ app: appName }) }
|
||||
</button>
|
||||
</a>
|
||||
<a href = { _URLS[Platform.OS] }>
|
||||
<button className = { `${_SNS}__button` }>
|
||||
<button className = { downloadButtonClassName }>
|
||||
{ t(`${_TNS}.downloadApp`) }
|
||||
</button>
|
||||
</a>
|
||||
{ _room
|
||||
? <DialInSummary
|
||||
className = 'unsupported-dial-in'
|
||||
<a
|
||||
className = { `${_SNS}__href` }
|
||||
href = { this.state.joinURL }>
|
||||
{/* <button className = { `${_SNS}__button` }> */}
|
||||
{ t(`${_TNS}.openApp`) }
|
||||
{/* </button> */}
|
||||
</a>
|
||||
<DialInSummary
|
||||
className = 'deep-linking-dial-in'
|
||||
clickableNumbers = { true }
|
||||
room = { _room } />
|
||||
: null }
|
||||
</div>
|
||||
<HideNotificationBarStyle />
|
||||
</div>
|
||||
|
@ -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));
|
|
@ -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.
|
||||
</h2>
|
||||
<p className = { `${ns}__description` }>
|
||||
Please use { interfaceConfig.APP_NAME } on desktop to join
|
||||
calls.
|
||||
Please use { interfaceConfig.NATIVE_APP_NAME } on desktop to
|
||||
join calls.
|
||||
</p>
|
||||
|
||||
<HideNotificationBarStyle />
|
|
@ -0,0 +1,3 @@
|
|||
export { default as DeepLinkingDesktopPage } from './DeepLinkingDesktopPage';
|
||||
export { default as DeepLinkingMobilePage } from './DeepLinkingMobilePage';
|
||||
export { default as NoMobileApp } from './NoMobileApp';
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* The namespace of the i18n/translation keys.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const _TNS = 'deepLinking';
|
|
@ -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();
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export * from './functions';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
|
@ -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);
|
||||
});
|
|
@ -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;
|
||||
});
|
|
@ -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);
|
||||
}
|
|
@ -129,6 +129,7 @@ class DialInSummary extends Component {
|
|||
: null,
|
||||
<NumbersList
|
||||
clickableNumbers = { this.props.clickableNumbers }
|
||||
conferenceID = { conferenceID }
|
||||
key = 'numbers'
|
||||
numbers = { this.state.numbers } />
|
||||
];
|
||||
|
|
|
@ -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 (
|
||||
<a
|
||||
href = { `tel:${number}` }
|
||||
href = { `tel:${number}p${this.props.conferenceID}#` }
|
||||
key = { number } >
|
||||
{ number }
|
||||
</a>
|
||||
|
|
|
@ -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');
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
export * from './actions';
|
||||
export * from './components';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
|
|
|
@ -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;
|
||||
});
|
|
@ -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';
|
||||
|
||||
|
|
Loading…
Reference in New Issue