diff --git a/__mocks__/cssMock.js b/__mocks__/cssMock.js new file mode 100644 index 00000000..9b5d9b34 --- /dev/null +++ b/__mocks__/cssMock.js @@ -0,0 +1 @@ +module.exports = "css-file-stub"; diff --git a/package.json b/package.json index 57d8513f..76576363 100644 --- a/package.json +++ b/package.json @@ -215,6 +215,7 @@ "/node_modules/matrix-react-sdk/test/setupTests.js" ], "moduleNameMapper": { + "\\.(css|scss)$": "/__mocks__/cssMock.js", "\\.(gif|png|svg|ttf|woff2)$": "/node_modules/matrix-react-sdk/__mocks__/imageMock.js", "\\$webapp/i18n/languages.json": "/node_modules/matrix-react-sdk/__mocks__/languages.json", "^browser-request$": "/node_modules/matrix-react-sdk/__mocks__/browser-request.js", diff --git a/res/css/structures/ErrorView.scss b/res/css/structures/ErrorView.scss new file mode 100644 index 00000000..f75ee695 --- /dev/null +++ b/res/css/structures/ErrorView.scss @@ -0,0 +1,101 @@ +/* +Copyright 2020 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// import font-size variables manually, ideally this scss would get loaded by the theme which has all variables in context +@import "../../../node_modules/matrix-react-sdk/res/css/_font-sizes.scss"; + +.mx_ErrorView { + background: #c5e0f7; + background: -moz-linear-gradient(top, #c5e0f7 0%, #ffffff 100%); + background: -webkit-linear-gradient(top, #c5e0f7 0%, #ffffff 100%); + background: linear-gradient(to bottom, #c5e0f7 0%, #ffffff 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#c5e0f7', endColorstr='#ffffff',GradientType=0 ); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + width: 100%; + min-height: 100%; + height: auto; + color: #000; + + .mx_ErrorView_container { + max-width: 680px; + margin: auto; + } + + .mx_Button { + border: 0; + border-radius: 4px; + font-size: $font-18px; + margin-left: 4px; + margin-right: 4px; + min-width: 80px; + background-color: #03B381; + color: #fff; + cursor: pointer; + padding: 12px 22px; + word-break: break-word; + text-decoration: none; + } + + .mx_Center { + justify-content: center; + } + + .mx_HomePage_header { + color: #2E2F32; + display: flex; + align-items: center; + justify-content: center; + } + + font-size: $font-16px; + h1 { + font-size: $font-32px; + } + h2 { + font-size: $font-24px; + color: #000; + } + + .mx_HomePage_col { + display: flex; + flex-direction: row; + } + + .mx_HomePage_row { + flex: 1 1 0; + display: flex; + flex-direction: row; + flex-wrap: wrap; + } + + .mx_HomePage_logo { + margin: auto 20px auto 0; + } + + h1, h2 { + font-weight: 600; + margin-bottom: 32px; + } + + .mx_Spacer { + margin-top: 24px; + } + + .mx_FooterLink { + color: #368BD6; + text-decoration: none; + } +} diff --git a/res/themes/riot/img/download/apple.svg b/res/themes/riot/img/download/apple.svg new file mode 100644 index 00000000..9de39edc --- /dev/null +++ b/res/themes/riot/img/download/apple.svg @@ -0,0 +1,97 @@ + + Download on the App Store. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/themes/riot/img/download/fdroid.svg b/res/themes/riot/img/download/fdroid.svg new file mode 100644 index 00000000..847196f5 --- /dev/null +++ b/res/themes/riot/img/download/fdroid.svg @@ -0,0 +1,135 @@ + + Get it on F-Droid. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/themes/riot/img/download/google.svg b/res/themes/riot/img/download/google.svg new file mode 100644 index 00000000..d54aca16 --- /dev/null +++ b/res/themes/riot/img/download/google.svg @@ -0,0 +1,70 @@ + + Download on the Google Play Store. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index a3fb9028..9716cafe 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -15,7 +15,7 @@ limitations under the License. */ import "matrix-react-sdk/src/@types/global"; // load matrix-react-sdk's type extensions first -import {Renderer} from "react-dom"; +import type {Renderer} from "react-dom"; declare global { interface Window { diff --git a/src/async-components/structures/CompatibilityView.tsx b/src/async-components/structures/CompatibilityView.tsx new file mode 100644 index 00000000..b032214a --- /dev/null +++ b/src/async-components/structures/CompatibilityView.tsx @@ -0,0 +1,105 @@ +/* +Copyright 2020 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as React from "react"; +import { _t } from "matrix-react-sdk/src/languageHandler"; + +// directly import the style here as this layer does not support rethemedex at this time so no matrix-react-sdk +// scss variables will be accessible. +import "../../../res/css/structures/ErrorView.scss"; + +interface IProps { + onAccept(): void; +} + +const CompatibilityView: React.FC = ({ onAccept }) => { + return
+
+
+ + Riot + +

{ _t("Unsupported browser") }

+
+ +
+
+
+

{ _t("Your browser can't run Riot") }

+

+ { _t( + "Riot uses advanced browser features which aren't supported by your current browser.", + ) } +

+

+ { _t( + 'Please install Chrome, Firefox, ' + + 'or Safari for the best experience.', + {}, + { + 'chromeLink': (sub) => {sub}, + 'firefoxLink': (sub) => {sub}, + 'safariLink': (sub) => {sub}, + }, + )} +

+

+ { _t( + "You can continue using your current browser, but some or all features may not work " + + "and the look and feel of the application may be incorrect.", + ) } +

+ +
+
+
+ +
+
+
+

Use Riot on mobile

+

iOS (iPhone or iPad)

+ + Apple App Store + +

Android

+ + Google Play Store + + + F-Droid + +
+
+
+ + +
+
; +}; + +export default CompatibilityView; diff --git a/src/async-components/structures/ErrorView.tsx b/src/async-components/structures/ErrorView.tsx new file mode 100644 index 00000000..3c2d1e41 --- /dev/null +++ b/src/async-components/structures/ErrorView.tsx @@ -0,0 +1,61 @@ +/* +Copyright 2020 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as React from "react"; +import { _t } from "matrix-react-sdk/src/languageHandler"; + +// directly import the style here as this layer does not support rethemedex at this time so no matrix-react-sdk +// scss variables will be accessible. +import "../../../res/css/structures/ErrorView.scss"; + +interface IProps { + // both of these should already be internationalised + title: string; + messages?: string[]; +} + +const ErrorView: React.FC = ({title, messages}) => { + return
+
+
+ + Riot + +

{ _t("Failed to start") }

+
+
+
+
+

{ title }

+ {messages && messages.map(msg =>

+ { msg } +

)} +
+
+
+ +
+
; +}; + +export default ErrorView; + diff --git a/src/components/structures/ErrorView.tsx b/src/components/structures/ErrorView.tsx deleted file mode 100644 index 6941dbf1..00000000 --- a/src/components/structures/ErrorView.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright 2020 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import * as React from "react"; -import * as PropTypes from "prop-types"; - -import { _t } from "matrix-react-sdk/src/languageHandler"; - -interface IProps { - title: string; - messages?: string[]; -} - -const ErrorView: React.FC = ({title, messages}) => { - return
-
-

{title}

-
- {messages && messages.map(msg =>

- { _t(msg) } -

)} -
-
-
; -}; - -ErrorView.propTypes = { - title: PropTypes.string.isRequired, - messages: PropTypes.arrayOf(PropTypes.string.isRequired), -}; - -export default ErrorView; - diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 89f999de..3461eeb7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -19,6 +19,14 @@ "Custom Server Options": "Custom Server Options", "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Riot with an existing Matrix account on a different homeserver.": "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Riot with an existing Matrix account on a different homeserver.", "Dismiss": "Dismiss", + "Unsupported browser": "Unsupported browser", + "Your browser can't run Riot": "Your browser can't run Riot", + "Riot uses advanced browser features which aren't supported by your current browser.": "Riot uses advanced browser features which aren't supported by your current browser.", + "Please install Chrome, Firefox, or Safari for the best experience.": "Please install Chrome, Firefox, or Safari for the best experience.", + "You can continue using your current browser, but some or all features may not work and the look and feel of the application may be incorrect.": "You can continue using your current browser, but some or all features may not work and the look and feel of the application may be incorrect.", + "I understand the risks and wish to continue": "I understand the risks and wish to continue", + "Go to Riot.im": "Go to Riot.im", + "Failed to start": "Failed to start", "Welcome to Riot.im": "Welcome to Riot.im", "Decentralised, encrypted chat & collaboration powered by [matrix]": "Decentralised, encrypted chat & collaboration powered by [matrix]", "Sign In": "Sign In", diff --git a/src/vector/app.js b/src/vector/app.tsx similarity index 93% rename from src/vector/app.js rename to src/vector/app.tsx index 930576e2..b665feda 100644 --- a/src/vector/app.js +++ b/src/vector/app.tsx @@ -19,9 +19,9 @@ limitations under the License. */ import React from 'react'; -// add React and ReactPerf to the global namespace, to make them easier to -// access via the console -global.React = React; +// add React and ReactPerf to the global namespace, to make them easier to access via the console +// this incidentally means we can forget our React imports in JSX files without penalty. +window.React = React; import * as sdk from 'matrix-react-sdk'; import PlatformPeg from 'matrix-react-sdk/src/PlatformPeg'; @@ -30,6 +30,7 @@ import {_td, newTranslatableError} from 'matrix-react-sdk/src/languageHandler'; import AutoDiscoveryUtils from 'matrix-react-sdk/src/utils/AutoDiscoveryUtils'; import {AutoDiscovery} from "matrix-js-sdk/src/autodiscovery"; import * as Lifecycle from "matrix-react-sdk/src/Lifecycle"; +import type MatrixChatType from "matrix-react-sdk/src/components/structures/MatrixChat"; import url from 'url'; @@ -40,11 +41,11 @@ import SdkConfig from "matrix-react-sdk/src/SdkConfig"; import CallHandler from 'matrix-react-sdk/src/CallHandler'; -let lastLocationHashSet = null; +let lastLocationHashSet: string = null; // Parse the given window.location and return parameters that can be used when calling // MatrixChat.showScreen(screen, params) -function getScreenFromLocation(location) { +function getScreenFromLocation(location: Location) { const fragparts = parseQsFromFragment(location); return { screen: fragparts.location.substring(1), @@ -54,15 +55,15 @@ function getScreenFromLocation(location) { // Here, we do some crude URL analysis to allow // deep-linking. -function routeUrl(location) { +function routeUrl(location: Location) { if (!window.matrixChat) return; console.log("Routing URL ", location.href); const s = getScreenFromLocation(location); - window.matrixChat.showScreen(s.screen, s.params); + (window.matrixChat as MatrixChatType).showScreen(s.screen, s.params); } -function onHashChange(ev) { +function onHashChange(ev: HashChangeEvent) { if (decodeURIComponent(window.location.hash) === lastLocationHashSet) { // we just set this: no need to route it! return; @@ -72,8 +73,8 @@ function onHashChange(ev) { // This will be called whenever the SDK changes screens, // so a web page can update the URL bar appropriately. -function onNewScreen(screen) { - console.log("newscreen "+screen); +function onNewScreen(screen: string) { + console.log("newscreen " + screen); const hash = '#/' + screen; lastLocationHashSet = hash; window.location.hash = hash; @@ -88,7 +89,7 @@ function onNewScreen(screen) { // If we're in electron, we should never pass through a file:// URL otherwise // the identity server will try to 302 the browser to it, which breaks horribly. // so in that instance, hardcode to use riot.im/app for now instead. -function makeRegistrationUrl(params) { +function makeRegistrationUrl(params: object) { let url; if (window.location.protocol === "vector:") { url = 'https://riot.im/app/#/register'; @@ -121,8 +122,7 @@ function onTokenLoginCompleted() { const parsedUrl = url.parse(window.location.href); parsedUrl.search = ""; const formatted = url.format(parsedUrl); - console.log("Redirecting to " + formatted + " to drop loginToken " + - "from queryparams"); + console.log(`Redirecting to ${formatted} to drop loginToken from queryparams`); window.location.href = formatted; } diff --git a/src/vector/index.html b/src/vector/index.html index 85b6a1bd..daa1290d 100644 --- a/src/vector/index.html +++ b/src/vector/index.html @@ -49,7 +49,7 @@ <% } } %> - +
diff --git a/src/vector/index.ts b/src/vector/index.ts index c8205873..b3e25a58 100644 --- a/src/vector/index.ts +++ b/src/vector/index.ts @@ -78,10 +78,7 @@ function checkBrowserFeatures() { return featureComplete; } -let acceptBrowser = checkBrowserFeatures(); -if (!acceptBrowser && window.localStorage) { - acceptBrowser = Boolean(window.localStorage.getItem("mx_accepts_unsupported_browser")); -} +const supportedBrowser = checkBrowserFeatures(); // React depends on Map & Set which we check for using modernizr's es6collections // if modernizr fails we may not have a functional react to show the error message. @@ -148,6 +145,11 @@ async function start() { // await things settling so that any errors we have to render have features like i18n running await settled(loadSkinPromise, loadThemePromise, loadLanguagePromise); + let acceptBrowser = supportedBrowser; + if (!acceptBrowser && window.localStorage) { + acceptBrowser = Boolean(window.localStorage.getItem("mx_accepts_unsupported_browser")); + } + // ########################## // error handling begins here // ########################## @@ -199,9 +201,23 @@ async function start() { ]); } } + start().catch(err => { console.error(err); - if (!acceptBrowser) { - // TODO redirect to static incompatible browser page - } + // show the static error in an iframe to not lose any context / console data + // with some basic styling to make the iframe full page + delete document.body.style.height; + const iframe = document.createElement("iframe"); + // @ts-ignore - typescript seems to only like the IE syntax for iframe sandboxing + iframe["sandbox"] = ""; + iframe.src = supportedBrowser ? "static/unable-to-load.html" : "static/incompatible-browser.html"; + iframe.style.width = "100%"; + iframe.style.height = "100%"; + iframe.style.position = "absolute"; + iframe.style.top = "0"; + iframe.style.left = "0"; + iframe.style.right = "0"; + iframe.style.bottom = "0"; + iframe.style.border = "0"; + document.getElementById("matrixchat").appendChild(iframe); }); diff --git a/src/vector/init.tsx b/src/vector/init.tsx index c7c9141e..82c5931b 100644 --- a/src/vector/init.tsx +++ b/src/vector/init.tsx @@ -148,18 +148,16 @@ export async function loadApp(fragParams: {}) { export async function showError(title: string, messages?: string[]) { const ErrorView = (await import( /* webpackChunkName: "error-view" */ - /* webpackPreload: true */ - "../components/structures/ErrorView")).default; + "../async-components/structures/ErrorView")).default; window.matrixChat = ReactDOM.render(, document.getElementById('matrixchat')); } export async function showIncompatibleBrowser(onAccept) { - const CompatibilityPage = (await import( - /* webpackChunkName: "compatibility-page" */ - /* webpackPreload: true */ - "matrix-react-sdk/src/components/structures/CompatibilityPage")).default; - window.matrixChat = ReactDOM.render(, + const CompatibilityView = (await import( + /* webpackChunkName: "compatibility-view" */ + "../async-components/structures/CompatibilityView")).default; + window.matrixChat = ReactDOM.render(, document.getElementById('matrixchat')); } diff --git a/src/vector/static/incompatible-browser.html b/src/vector/static/incompatible-browser.html new file mode 100644 index 00000000..69287949 --- /dev/null +++ b/src/vector/static/incompatible-browser.html @@ -0,0 +1,492 @@ + + + + + + + + + + +
+
+ +

Unsupported browser

+
+
+
+
+

Your browser can't run Riot

+

Riot uses many advanced browser features, some of which are not available or experimental in your current browser.

+

Please install Chrome, Firefox, + or Safari for the best experience.

+
+
+
+ + +
+ + diff --git a/src/vector/static/unable-to-load.html b/src/vector/static/unable-to-load.html new file mode 100644 index 00000000..f5219653 --- /dev/null +++ b/src/vector/static/unable-to-load.html @@ -0,0 +1,199 @@ + + + + + + + + + + + +
+ +
+ +
+
+ +

Unable to load

+
+
+
+
+

Riot can't load

+

Something went wrong and riot was unable to load.

+
+
+
+ +
+ + diff --git a/test/app-tests/loading-test.js b/test/app-tests/loading-test.js index 5418382d..beb62270 100644 --- a/test/app-tests/loading-test.js +++ b/test/app-tests/loading-test.js @@ -28,7 +28,7 @@ import MatrixReactTestUtils from 'matrix-react-test-utils'; import * as jssdk from 'matrix-js-sdk'; import * as sdk from 'matrix-react-sdk'; import {MatrixClientPeg} from 'matrix-react-sdk/src/MatrixClientPeg'; -import {VIEWS} from 'matrix-react-sdk/src/components/structures/MatrixChat'; +import {Views} from 'matrix-react-sdk/src/components/structures/MatrixChat'; import dis from 'matrix-react-sdk/src/dispatcher'; import * as test_utils from '../test-utils'; import MockHttpBackend from 'matrix-mock-request'; @@ -679,7 +679,7 @@ function assertAtLoadingSpinner(matrixChat) { } function awaitLoggedIn(matrixChat) { - if (matrixChat.state.view === VIEWS.LOGGED_IN) { + if (matrixChat.state.view === Views.LOGGED_IN) { return Promise.resolve(); } return new Promise(resolve => { @@ -704,7 +704,7 @@ function awaitRoomView(matrixChat, retryLimit, retryCount) { retryCount = 0; } - if (matrixChat.state.view !== VIEWS.LOGGED_IN || !matrixChat.state.ready) { + if (matrixChat.state.view !== Views.LOGGED_IN || !matrixChat.state.ready) { console.log(Date.now() + " Awaiting room view: not ready yet."); if (retryCount >= retryLimit) { throw new Error("MatrixChat still not ready after " + diff --git a/webpack.config.js b/webpack.config.js index 5bde203e..9c6d0993 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -335,6 +335,20 @@ module.exports = (env, argv) => { chunks: ['mobileguide'], }), + // These are the static error pages for when the javascript env is *really unsupported* + new HtmlWebpackPlugin({ + template: './src/vector/static/unable-to-load.html', + filename: 'static/unable-to-load.html', + minify: argv.mode === 'production', + chunks: [], + }), + new HtmlWebpackPlugin({ + template: './src/vector/static/incompatible-browser.html', + filename: 'static/incompatible-browser.html', + minify: argv.mode === 'production', + chunks: [], + }), + // This is the usercontent sandbox's entry point (separate for iframing) new HtmlWebpackPlugin({ template: './node_modules/matrix-react-sdk/src/usercontent/index.html',