Introduce unsupported browser page

This commit is contained in:
Ilya Daynatovich 2017-01-19 17:47:22 +02:00 committed by Lyubomir Marinov
parent 57ba702dda
commit 2e81b8493e
17 changed files with 354 additions and 313 deletions

View File

@ -8,7 +8,6 @@ OUTPUT_DIR = .
STYLES_BUNDLE = css/all.bundle.css
STYLES_DESTINATION = css/all.css
STYLES_MAIN = css/main.scss
STYLES_UNSUPPORTED_BROWSER = css/unsupported_browser.scss
WEBPACK = ./node_modules/.bin/webpack
all: update-deps compile deploy clean
@ -47,7 +46,6 @@ deploy-lib-jitsi-meet:
$(DEPLOY_DIR)
deploy-css:
$(NODE_SASS) css/unsupported_browser.scss css/unsupported_browser.css ; \
$(NODE_SASS) $(STYLES_MAIN) $(STYLES_BUNDLE) && \
$(CLEANCSS) $(STYLES_BUNDLE) > $(STYLES_DESTINATION) ; \
rm $(STYLES_BUNDLE)
@ -59,6 +57,5 @@ source-package:
mkdir -p source_package/jitsi-meet/css && \
cp -r *.js *.html connection_optimization favicon.ico fonts images libs sounds LICENSE lang source_package/jitsi-meet && \
cp css/all.css source_package/jitsi-meet/css && \
cp css/unsupported_browser.css source_package/jitsi-meet/css && \
(cd source_package ; tar cjf ../jitsi-meet.tar.bz2 jitsi-meet) && \
rm -rf source_package

View File

@ -131,6 +131,6 @@ $linkHoverFontColor: #287ade;
/**
* Landing
*/
$landingButtonBgColor: #ff9a00;
$landingTextColor: #4a4a4a;
$primaryLandingButtonBgColor: #17a0db;
$unsupportedBrowserButtonBgColor: #ff9a00;
$unsupportedBrowserTextColor: #4a4a4a;
$primaryUnsupportedBrowserButtonBgColor: #17a0db;

View File

@ -67,6 +67,7 @@
@import '404';
@import 'policy';
@import 'filmstrip';
@import 'landing';
@import 'unsupported-browser/mobile-browser-page';
@import 'unsupported-browser/unsupported_browser';
/* Modules END */

View File

@ -1,11 +1,11 @@
.landing {
.mobile-browser-page {
background-color: #fff;
height: 100vh;
padding: 35px 0;
width: 100vw;
&__body {
color: $landingTextColor;
color: $unsupportedBrowserTextColor;
margin: auto;
max-width: 40em;
text-align: center;
@ -44,25 +44,25 @@
max-width: 300px;
width: 98%;
@include border-radius(8px);
background-color: $landingButtonBgColor;
background-color: $unsupportedBrowserButtonBgColor;
font-size: 1.5em;
font-weight: 300;
letter-spacing: 0.5px;
text-shadow: 0px 1px 2px $landingTextColor;
text-shadow: 0px 1px 2px $unsupportedBrowserTextColor;
// Disable standard button effects.
box-shadow: none;
outline: none;
&:active {
background-color: $landingButtonBgColor;
background-color: $unsupportedBrowserButtonBgColor;
}
&_primary {
background-color: $primaryLandingButtonBgColor;
background-color: $primaryUnsupportedBrowserButtonBgColor;
&:active {
background-color: $primaryLandingButtonBgColor;
background-color: $primaryUnsupportedBrowserButtonBgColor;
}
}
}

View File

@ -0,0 +1,132 @@
.browser {
display: inline-block;
margin: 1em 7px;
width: 138px;
vertical-align: middle;
color: #929391;
font-size: 20px;
&__button {
background-color: #62c82a;
border: 1px solid #3c8117;
border-radius: 10px;
color: #FFFFFF;
font-size: 12px;
text-align: center;
width: 115px;
height: 26px;
padding-top: 13px;
margin: 15px auto 0px auto;
}
&__link {
color: #087dba;
text-decoration: none;
&:hover {
text-decoration: none;
}
&:active {
text-decoration: none;
}
&:focus {
text-decoration: none;
}
}
&-list
{
margin: 0 auto;
}
&__logo {
margin: 20px auto 0px auto;
&_chrome {
width: 78px;
height: 78px;
background-image: url('../../images/chrome.png');
}
&_chromium {
width: 77px;
height: 78px;
background-image: url('../../images/chromium.png');
}
&_firefox {
width: 86px;
height: 80px;
background-image: url('../../images/firefox.png');
}
&_opera {
width: 73px;
height: 78px;
background-image: url('../../images/opera.png');
}
&_ie {
width: 80px;
height: 78px;
background-image: url('../../images/ie.png');
}
&_safari {
width: 78px;
height: 79px;
background-image: url('../../images/safari.png');
}
}
&__text
{
line-height: 1.2em;
&_small {
font-size: small;
}
}
&__tile {
width: 138px;
height: 163px;
margin-top: 5px;
background-color: #e8e8e8;
border: 1px solid #cfcfcf;
border-radius: 10px;
}
}
.unsupported-browser {
display: block;
position: absolute;
width:500px;
height: 565px;
overflow:hidden;
text-align: center;
margin: auto;
top: 0; left: 0; bottom: 0; right: 0;
&__page {
display:inline-block;
font-size: 28px;
vertical-align:middle;
padding-top: 25px;
}
&__title {
margin: 0 auto;
width: 20em;
}
&-wrapper {
display: block;
position: absolute;
width: 100%;
height: 100%;
background: #fff;
}
}

View File

@ -1,138 +0,0 @@
@import 'variables';
body {
width:100%;
height:100%;
background-color: white;
color: #424242;
font-family: $baseFontFamily;
font-size: 28px;
margin:0;
padding:0;
}
#wrap{
display: block;
position: absolute;
width:500px;
height: 565px;
overflow:hidden;
text-align: center;
margin: auto;
top: 0; left: 0; bottom: 0; right: 0;
}
.firefox{
font-size: 11pt;
color: #c8c8c8;
width: 468px;
text-align: center;
margin: 30px auto 0px auto;
padding-left: 15px;
}
#text{
display:inline-block;
font-size: 28px;
/* width: 568px; */
vertical-align:middle;
padding-top: 25px;
}
a {
color: #087dba;
text-decoration:none;
}
.browser {
width: 138px;
height: 163px;
margin-top: 5px;
background-color: #e8e8e8;
border: 1px solid #cfcfcf;
border-radius: 10px;
}
.browser_wrapper
{
width: 138px;
/* height: 188px; */
vertical-align: middle;
color: #929391;
font-size: 20px;
float: left;
margin-left: 15px;
margin-top: 5px;
}
.browser_text
{
height: 2em;
}
.supported_browsers
{
margin: 0px auto 0px auto;
/* width: 660px; */
}
.clear
{
clear: both;
}
.button
{
background-color: #62c82a;
border: 1px solid #3c8117;
border-radius: 10px;
color: #FFFFFF;
font-size: 12px;
text-align: center;
width: 115px;
height: 26px;
padding-top: 13px;
margin: 15px auto 0px auto;
}
.logo
{
margin: 20px auto 0px auto;
}
#chrome_logo
{
width: 78px;
height: 78px;
background-image: url('../images/chrome.png');
}
#chromium_logo
{
width: 77px;
height: 78px;
background-image: url('../images/chromium.png');
}
#firefox_logo
{
width: 86px;
height: 80px;
background-image: url('../images/firefox.png');
}
#opera_logo
{
width: 73px;
height: 78px;
background-image: url('../images/opera.png');
}
#safari_logo
{
width: 78px;
height: 79px;
background-image: url('../images/safari.png');
}
#ie_logo
{
width: 80px;
height: 78px;
background-image: url('../images/ie.png');
}

View File

@ -6,19 +6,10 @@ import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
import {
_getRoomAndDomainFromUrlString,
_getRouteToRender,
areRoutesEqual,
init
} from './functions';
import './reducer';
/**
* Variable saving current route of the app. Later it will be extracted as
* a property of the class with Router specific logic.
*
* @type {Object}
*/
let currentRoute = {};
/**
* Temporary solution. Should dispatch actions related to initial settings of
* the app like setting log levels, reading the config parameters from query
@ -73,8 +64,8 @@ export function appNavigate(urlOrRoom) {
.then(() => {
const link = typeof room === 'undefined'
&& typeof domain === 'undefined'
? urlOrRoom
: room;
? urlOrRoom
: room;
dispatch(_setRoomAndNavigate(link));
});
@ -153,9 +144,6 @@ function _setRoomAndNavigate(newRoom) {
const { app } = state['features/app'];
const newRoute = _getRouteToRender(state);
if (!areRoutesEqual(newRoute, currentRoute)) {
currentRoute = newRoute;
app._navigate(newRoute);
}
app._navigate(newRoute);
};
}

View File

@ -5,6 +5,7 @@ import {
localParticipantJoined,
localParticipantLeft
} from '../../base/participants';
import { RouteRegistry } from '../../base/navigator';
import {
appNavigate,
@ -31,7 +32,7 @@ export class AbstractApp extends Component {
* The URL, if any, with which the app was launched.
*/
url: React.PropTypes.string
}
};
/**
* Initializes a new App instance.
@ -210,33 +211,38 @@ export class AbstractApp extends Component {
* @returns {void}
*/
_navigate(route) {
let nextState = {
...this.state,
route
};
const currentRoute = this.state.route || {};
// The Web App was using react-router so it utilized react-router's
// onEnter. During the removal of react-router, modifications were
// minimized by preserving the onEnter interface:
// (1) Router would provide its nextState to the Route's onEnter. As the
// role of Router is now this AbstractApp, provide its nextState.
// (2) A replace function would be provided to the Route in case it
// chose to redirect to another path.
this._onRouteEnter(route, nextState, pathname => {
// FIXME In order to minimize the modifications related to the
// removal of react-router, the Web implementation is provided
// bellow because the replace function is used on Web only at the
// time of this writing. Provide a platform-agnostic implementation.
// It should likely find the best Route matching the specified
// pathname and navigate to it.
window.location.pathname = pathname;
if (!RouteRegistry.areRoutesEqual(route, currentRoute)) {
let nextState = {
...this.state,
route
};
// Do not proceed with the route because it chose to redirect to
// another path.
nextState = undefined;
});
// The Web App was using react-router so it utilized react-router's
// onEnter. During the removal of react-router, modifications were
// minimized by preserving the onEnter interface:
// (1) Router would provide its nextState to the Route's onEnter.
// As the role of Router is now this AbstractApp, provide its
// nextState.
// (2) A replace function would be provided to the Route in case it
// chose to redirect to another path.
this._onRouteEnter(route, nextState, pathname => {
// FIXME In order to minimize the modifications related to the
// removal of react-router, the Web implementation is provided
// bellow because the replace function is used on Web only at
// the time of this writing. Provide a platform-agnostic
// implementation. It should likely find the best Route matching
// the specified pathname and navigate to it.
window.location.pathname = pathname;
nextState && this.setState(nextState);
// Do not proceed with the route because it chose to redirect to
// another path.
nextState = undefined;
});
nextState && this.setState(nextState);
}
}
/**

View File

@ -4,7 +4,7 @@ import { isRoomValid } from '../base/conference';
import { RouteRegistry } from '../base/navigator';
import { Platform } from '../base/react';
import { Conference } from '../conference';
import { Landing } from '../unsupported-browser';
import { MobileBrowserPage } from '../unsupported-browser';
import { WelcomePage } from '../welcome';
import URLProcessor from '../../../modules/config/URLProcessor';
@ -33,37 +33,19 @@ export function _getRouteToRender(stateOrGetState) {
? stateOrGetState()
: stateOrGetState;
// If landing was shown, there is no need to show it again.
const { landingIsShown } = state['features/unsupported-browser'];
// If mobile browser page was shown, there is no need to show it again.
const { mobileBrowserPageIsShown } = state['features/unsupported-browser'];
const { room } = state['features/base/conference'];
const component = isRoomValid(room) ? Conference : WelcomePage;
const route = RouteRegistry.getRouteByComponent(component);
// We're using spread operator here to create copy of the route registered
// in registry. If we overwrite some of its properties (like 'component')
// they will stay unchanged in the registry.
const route = { ...RouteRegistry.getRouteByComponent(component) };
if ((OS === 'android' || OS === 'ios') && !landingIsShown) {
route.component = Landing;
if ((OS === 'android' || OS === 'ios') && !mobileBrowserPageIsShown) {
route.component = MobileBrowserPage;
}
return route;
}
/**
* Method checking whether route objects are equal by value. Returns true if
* and only if key values of the first object are equal to key values of
* the second one.
*
* @param {Object} newRoute - New route object to be compared.
* @param {Object} oldRoute - Old route object to be compared.
* @returns {boolean}
*/
export function areRoutesEqual(newRoute, oldRoute) {
return Object.keys(newRoute)
.every(key => newRoute[key] === oldRoute[key]);
}
/**
* Temporary solution. Later we'll get rid of global APP and set its properties
* in redux store.

View File

@ -11,6 +11,20 @@
* without needing to create additional inter-feature dependencies.
*/
class RouteRegistry {
/**
* Method checking whether route objects are equal by value. Returns true if
* and only if key values of the first object are equal to key values of
* the second one.
*
* @param {Object} newRoute - New route object to be compared.
* @param {Object} oldRoute - Old route object to be compared.
* @returns {boolean}
*/
areRoutesEqual(newRoute, oldRoute) {
return Object.keys(newRoute)
.every(key => newRoute[key] === oldRoute[key]);
}
/**
* Initializes a new RouteRegistry instance.
*/

View File

@ -1,10 +1,12 @@
import { Symbol } from '../base/react';
/**
* The type of the Redux action which signals that a mobile landing is shown.
* The type of the Redux action which signals that a mobile browser page
* is shown.
*
* {
* type: LANDING_IS_SHOWN
* type: MOBILE_BROWSER_PAGE_IS_SHOWN
* }
*/
export const LANDING_IS_SHOWN = Symbol('LANDING_IS_SHOWN');
// eslint-disable-next-line max-len
export const MOBILE_BROWSER_PAGE_IS_SHOWN = Symbol('MOBILE_BROWSER_PAGE_IS_SHOWN');

View File

@ -1,16 +1,16 @@
import { LANDING_IS_SHOWN } from './actionTypes';
import { MOBILE_BROWSER_PAGE_IS_SHOWN } from './actionTypes';
import './reducer';
/**
* Returns an action that mobile landing is shown
* and there is no need to show it on other pages.
* Returns an action that mobile browser page is shown and there is no need
* to show it on other pages.
*
* @returns {{
* type: LANDING_IS_SHOWN
* type: MOBILE_BROWSER_PAGE_IS_SHOWN
* }}
*/
export function landingIsShown() {
export function mobileBrowserPageIsShown() {
return {
type: LANDING_IS_SHOWN
type: MOBILE_BROWSER_PAGE_IS_SHOWN
};
}

View File

@ -4,7 +4,7 @@ import { connect } from 'react-redux';
import { Platform } from '../../base/react';
import { appNavigate } from '../../app';
import { landingIsShown } from '../actions';
import { mobileBrowserPageIsShown } from '../actions';
/**
* The map of platforms to URLs at which the mobile app for the associated
@ -16,14 +16,14 @@ const URLS = {
};
/**
* React component representing mobile landing page.
* React component representing mobile browser page.
*
* @class Landing
* @class MobileBrowserPage
*/
class Landing extends Component {
class MobileBrowserPage extends Component {
/**
* Constructor of Landing component.
* Constructor of MobileBrowserPage component.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
@ -36,14 +36,14 @@ class Landing extends Component {
}
/**
* Landing component's property types.
* Mobile browser page component's property types.
*
* @static
*/
static propTypes = {
dispatch: React.PropTypes.func,
room: React.PropTypes.string
}
};
/**
* React lifecycle method triggered after component is mounted.
@ -51,7 +51,7 @@ class Landing extends Component {
* @returns {void}
*/
componentDidMount() {
this.props.dispatch(landingIsShown());
this.props.dispatch(mobileBrowserPageIsShown());
}
/**
@ -90,21 +90,25 @@ class Landing extends Component {
}
/**
* Renders landing component.
* Renders component.
*
* @returns {ReactElement}
*/
render() {
const { btnText } = this.state;
const primaryButtonClasses = 'landing__button landing__button_primary';
const blockPrefix = 'mobile-browser-page';
const textClasses = `${blockPrefix}__text ${blockPrefix}__text_small`;
let primaryButtonClasses = `${blockPrefix}__button`;
primaryButtonClasses += `${blockPrefix}__button_primary`;
return (
<div className = 'landing'>
<div className = 'landing__body'>
<div className = { blockPrefix }>
<div className = { `${blockPrefix}__body` }>
<img
className = 'landing__logo'
className = { `${blockPrefix}__logo` }
src = '/images/logo-blue.svg' />
<p className = 'landing__text'>
<p className = { `${blockPrefix}__text` }>
You need <strong>Jitsi Meet</strong> to join a
conversation on your mobile
</p>
@ -113,13 +117,13 @@ class Landing extends Component {
Download the App
</button>
</a>
<p className = 'landing__text landing__text_small'>
<p className = { textClasses }>
or if you already have it
<br />
<strong>then</strong>
</p>
<button
className = 'landing__button'
className = 'mobile-browser-page__button'
onClick = { this._onClickJoin }>
{ btnText }
</button>
@ -130,7 +134,7 @@ class Landing extends Component {
}
/**
* Maps (parts of) the Redux state to the associated Landing's props.
* Maps (parts of) the Redux state to the associated MobileBrowserPage's props.
*
* @param {Object} state - Redux state.
* @returns {{
@ -143,4 +147,4 @@ function mapStateToProps(state) {
};
}
export default connect(mapStateToProps)(Landing);
export default connect(mapStateToProps)(MobileBrowserPage);

View File

@ -0,0 +1,117 @@
import React, { Component } from 'react';
/**
* Array of all supported browsers.
*/
const SUPPORTED_BROWSERS = [
{
link: 'http://google.com/chrome',
name: 'chrome',
plugin: false,
title: 'Chrome 44+'
}, {
link: 'http://www.chromium.org/',
name: 'chromium',
plugin: false,
title: 'Chromium 44+'
}, {
link: 'http://www.opera.com',
name: 'opera',
plugin: false,
title: 'Opera 32+'
}, {
link: 'http://www.getfirefox.com/',
name: 'firefox',
plugin: false,
title: 'Firefox and Iceweasel 40+'
}, {
link: 'https://temasys.atlassian.net/wiki/display/TWPP/WebRTC+Plugins',
name: 'ie',
plugin: 'Temasys 0.8.854+',
title: 'IE'
}, {
link: 'https://temasys.atlassian.net/wiki/display/TWPP/WebRTC+Plugins',
name: 'safari',
plugin: 'Temasys 0.8.854+',
title: 'Safari'
}
];
/**
* React component representing unsupported browser page.
*
* @class UnsupportedBrowserPage
*/
export default class UnsupportedBrowserPage extends Component {
/**
* Renders the component.
*
* @returns {ReactElement}
*/
render() {
return (
<div className = 'unsupported-browser-wrapper'>
<div className = 'unsupported-browser'>
<div className = 'unsupported-browser__content'>
<h2 className = 'unsupported-browser__title'>
This application is currently only supported by
</h2>
{ this._getSupportedBrowsersLayout() }
</div>
</div>
</div>
);
}
/**
* Generates layout for the list of supported browsers.
*
* @returns {ReactElement}
* @private
*/
_getSupportedBrowsersLayout() {
return (
<div className = 'browser-list'>
{ SUPPORTED_BROWSERS.map(this._getSupportedBrowser) }
</div>
);
}
/**
* Method that generated layout for supported browser object.
*
* @param {Object} browser - Object containing information about supported
* browser.
* @returns {ReactElement}
* @private
*/
_getSupportedBrowser(browser) {
let pluginHtml = null;
const logoClassName = `browser__logo browser__logo_${browser.name}`;
// Browsers not supporting WebRTC could support application
// with Temasys plugin installed.
if (browser.plugin) {
const className = 'browser__text_small';
pluginHtml = <p className = { className }>({ browser.plugin })</p>;
}
return (
<div className = 'browser'>
<div className = 'browser__text'>
{ browser.title }
{ pluginHtml }
</div>
<div className = 'browser__tile'>
<div className = { logoClassName } />
<a
className = 'browser__link'
href = { browser.link }>
<div className = 'browser__button'>DOWNLOAD</div>
</a>
</div>
</div>
);
}
}

View File

@ -1 +1,2 @@
export { default as Landing } from './Landing';
export { default as MobileBrowserPage } from './MobileBrowserPage';
export { default as UnsupportedBrowserPage } from './UnsupportedBrowserPage';

View File

@ -1,21 +1,21 @@
import { ReducerRegistry } from '../base/redux';
import { LANDING_IS_SHOWN } from './actionTypes';
import { MOBILE_BROWSER_PAGE_IS_SHOWN } from './actionTypes';
ReducerRegistry.register(
'features/unsupported-browser',
(state = {}, action) => {
switch (action.type) {
case LANDING_IS_SHOWN:
case MOBILE_BROWSER_PAGE_IS_SHOWN:
return {
...state,
/**
* Flag that shows that mobile landing is shown.
* Flag that shows that mobile browser page is shown.
*
* @type {boolean}
*/
landingIsShown: true
mobileBrowserPageIsShown: true
};
}

View File

@ -1,65 +0,0 @@
<html>
<head>
<title>Jitsi Meet: Unsupported Browser</title>
<link rel="stylesheet" type="text/css" media="screen" href="css/unsupported_browser.css" />
</head>
<body>
<!-- wrap starts here -->
<div id="wrap">
<div id="text">
<p>This application is currently only supported by</p>
<div class="supported_browsers">
<div class="browser_wrapper">
Chrome 44+
<div class="browser">
<div class="logo" id="chrome_logo"></div>
<a href="http://google.com/chrome"><div class="button">DOWNLOAD</div></a>
</div>
</div>
<div class="browser_wrapper">
Chromium 44+
<div class="browser">
<div class="logo" id="chromium_logo"></div>
<a href="http://www.chromium.org/"><div class="button">DOWNLOAD</div></a>
</div>
</div>
<div class="browser_wrapper">
Opera 32+
<div class="browser">
<div class="logo" id="opera_logo"></div>
<a href="http://www.opera.com"><div class="button">DOWNLOAD</div></a>
</div>
</div>
<div class="browser_wrapper">
<div class="browser_text">
Firefox and Iceweasel 40+</div>
<div class="browser">
<div class="logo" id="firefox_logo"></div>
<a href="http://www.getfirefox.com/"><div class="button">DOWNLOAD</div></a>
</div>
</div>
<div class="browser_wrapper">
<div class="browser_text">
IE <br /> <span style="font-size: small">(Temasys 0.8.854+)</span></div>
<div class="browser">
<div class="logo" id="ie_logo"></div>
<a href="https://temasys.atlassian.net/wiki/display/TWPP/WebRTC+Plugins"><div class="button">DOWNLOAD</div></a>
</div>
</div>
<div class="browser_wrapper">
<div class="browser_text">
Safari <br /> <span style="font-size: small">(Temasys 0.8.854+)</span></div>
<div class="browser">
<div class="logo" id="safari_logo"></div>
<a href="https://temasys.atlassian.net/wiki/display/TWPP/WebRTC+Plugins"><div class="button">DOWNLOAD</div></a>
</div>
</div>
</div>
<div class="clear"></div>
</div>
<!-- wrap ends here -->
</div>
</body>
</html>