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;
|
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 {
|
|
@ -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 */
|
||||||
|
|
|
@ -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 |
|
@ -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 */
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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 { 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));
|
|
@ -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 />
|
|
@ -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,
|
: 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 } />
|
||||||
];
|
];
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 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.
|
||||||
|
|
|
@ -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';
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
export * from './actions';
|
|
||||||
export * from './components';
|
export * from './components';
|
||||||
|
|
||||||
import './middleware';
|
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 { 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';
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue