diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index a19e20b5..6a5adec6 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ReactNode} from "react"; import "modernizr"; +import {Renderer} from "react-dom"; declare global { interface Window { @@ -25,7 +25,10 @@ declare global { }; mxSendRageshake: (text: string, withLogs?: boolean) => void; - matrixChat: ReactNode; + matrixChat: ReturnType; + + // electron-only + ipcRenderer: any; } // workaround for https://github.com/microsoft/TypeScript/issues/30933 diff --git a/src/components/structures/ErrorView.tsx b/src/components/structures/ErrorView.tsx new file mode 100644 index 00000000..6941dbf1 --- /dev/null +++ b/src/components/structures/ErrorView.tsx @@ -0,0 +1,46 @@ +/* +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 b77de122..a7f2f40c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1,11 +1,13 @@ { + "Missing indexeddb worker script!": "Missing indexeddb worker script!", + "Invalid configuration: can only specify one of default_server_config, default_server_name, or default_hs_url.": "Invalid configuration: can only specify one of default_server_config, default_server_name, or default_hs_url.", + "Invalid configuration: no default server specified.": "Invalid configuration: no default server specified.", + "Your Riot is misconfigured": "Your Riot is misconfigured", "Your Riot configuration contains invalid JSON. Please correct the problem and reload the page.": "Your Riot configuration contains invalid JSON. Please correct the problem and reload the page.", "The message from the parser is: %(message)s": "The message from the parser is: %(message)s", "Invalid JSON": "Invalid JSON", - "Your Riot is misconfigured": "Your Riot is misconfigured", + "Unable to load config file: please refresh the page to try again.": "Unable to load config file: please refresh the page to try again.", "Unexpected error preparing the app. See console for details.": "Unexpected error preparing the app. See console for details.", - "Invalid configuration: can only specify one of default_server_config, default_server_name, or default_hs_url.": "Invalid configuration: can only specify one of default_server_config, default_server_name, or default_hs_url.", - "Invalid configuration: no default server specified.": "Invalid configuration: no default server specified.", "Open user settings": "Open user settings", "Riot Desktop on %(platformName)s": "Riot Desktop on %(platformName)s", "Go to your browser to complete Sign In": "Go to your browser to complete Sign In", diff --git a/src/vector/app.js b/src/vector/app.js index 836b2ef1..930576e2 100644 --- a/src/vector/app.js +++ b/src/vector/app.js @@ -26,7 +26,7 @@ global.React = React; import * as sdk from 'matrix-react-sdk'; import PlatformPeg from 'matrix-react-sdk/src/PlatformPeg'; import * as VectorConferenceHandler from 'matrix-react-sdk/src/VectorConferenceHandler'; -import {_t, _td, newTranslatableError} from 'matrix-react-sdk/src/languageHandler'; +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"; @@ -37,10 +37,8 @@ import {parseQs, parseQsFromFragment} from './url_utils'; import {MatrixClientPeg} from 'matrix-react-sdk/src/MatrixClientPeg'; import SdkConfig from "matrix-react-sdk/src/SdkConfig"; -import {setTheme} from "matrix-react-sdk/src/theme"; import CallHandler from 'matrix-react-sdk/src/CallHandler'; -import {loadConfig, preparePlatform, loadLanguage, loadOlm} from "./init"; let lastLocationHashSet = null; @@ -128,7 +126,7 @@ function onTokenLoginCompleted() { window.location.href = formatted; } -export async function loadApp(fragParams: {}, acceptBrowser: boolean) { +export async function loadApp(fragParams: {}) { // XXX: the way we pass the path to the worker script from webpack via html in body's dataset is a hack // but alternatives seem to require changing the interface to passing Workers to js-sdk const vectorIndexeddbWorkerScript = document.body.dataset.vectorIndexeddbWorkerScript; @@ -137,99 +135,37 @@ export async function loadApp(fragParams: {}, acceptBrowser: boolean) { // the bundling. The js-sdk will just fall back to accessing // indexeddb directly with no worker script, but we want to // make sure the indexeddb script is present, so fail hard. - throw new Error("Missing indexeddb worker script!"); + throw newTranslatableError(_td("Missing indexeddb worker script!")); } MatrixClientPeg.setIndexedDbWorkerScript(vectorIndexeddbWorkerScript); CallHandler.setConferenceHandler(VectorConferenceHandler); window.addEventListener('hashchange', onHashChange); - await loadOlm(); - - // set the platform for react sdk - preparePlatform(); const platform = PlatformPeg.get(); - // Load the config from the platform - const configError = await loadConfig(); - - // Load language after loading config.json so that settingsDefaults.language can be applied - await loadLanguage(); - const params = parseQs(window.location); - // as quickly as we possibly can, set a default theme... - await setTheme(); - - // Now that we've loaded the theme (CSS), display the config syntax error if needed. - if (configError && configError.err && configError.err instanceof SyntaxError) { - const errorMessage = ( -
-

- {_t( - "Your Riot configuration contains invalid JSON. Please correct the problem " + - "and reload the page.", - )} -

-

- {_t( - "The message from the parser is: %(message)s", - {message: configError.err.message || _t("Invalid JSON")}, - )} -

-
- ); - - const GenericErrorPage = sdk.getComponent("structures.GenericErrorPage"); - return ; - } - const urlWithoutQuery = window.location.protocol + '//' + window.location.host + window.location.pathname; console.log("Vector starting at " + urlWithoutQuery); - if (configError) { - return
- Unable to load config file: please refresh the page to try again. -
; - } else if (acceptBrowser) { - platform.startUpdater(); - try { - // Don't bother loading the app until the config is verified - const config = await verifyServerConfig(); - const MatrixChat = sdk.getComponent('structures.MatrixChat'); - return ; - } catch (err) { - console.error(err); + platform.startUpdater(); - let errorMessage = err.translatedMessage - || _t("Unexpected error preparing the app. See console for details."); - errorMessage = {errorMessage}; - - // Like the compatibility page, AWOOOOOGA at the user - const GenericErrorPage = sdk.getComponent("structures.GenericErrorPage"); - return ; - } - } else { - console.error("Browser is missing required features."); - // take to a different landing page to AWOOOOOGA at the user - const CompatibilityPage = sdk.getComponent("structures.CompatibilityPage"); - return ; - } + // Don't bother loading the app until the config is verified + const config = await verifyServerConfig(); + const MatrixChat = sdk.getComponent('structures.MatrixChat'); + return ; } async function verifyServerConfig() { diff --git a/src/vector/index.ts b/src/vector/index.ts index d844827a..a4f494bd 100644 --- a/src/vector/index.ts +++ b/src/vector/index.ts @@ -33,11 +33,13 @@ if ('serviceWorker' in navigator) { navigator.serviceWorker.register('sw.js'); } -async function settled(prom: Promise) { - try { - await prom; - } catch (e) { - console.error(e); +async function settled(...promises: Array>) { + for (const prom of promises) { + try { + await prom; + } catch (e) { + console.error(e); + } } } @@ -76,47 +78,130 @@ function checkBrowserFeatures() { return featureComplete; } +let acceptBrowser = checkBrowserFeatures(); +if (!acceptBrowser && window.localStorage) { + acceptBrowser = Boolean(window.localStorage.getItem("mx_accepts_unsupported_browser")); +} + // 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. // try in react but fallback to an `alert` +// We start loading stuff but don't block on it until as late as possible to allow +// the browser to use as much parallelism as it can. +// Load parallelism is based on research in https://github.com/vector-im/riot-web/issues/12253 async function start() { // load init.ts async so that its code is not executed immediately and we can catch any exceptions - const {rageshakePromise, loadSkin, loadApp} = await import( + const { + rageshakePromise, + preparePlatform, + loadOlm, + loadConfig, + loadSkin, + loadLanguage, + loadTheme, + loadApp, + showError, + showIncompatibleBrowser, + _t, + } = await import( /* webpackChunkName: "init" */ /* webpackPreload: true */ "./init"); - await settled(rageshakePromise); // give rageshake a chance to load/fail + try { + await settled(rageshakePromise); // give rageshake a chance to load/fail - const fragparts = parseQsFromFragment(window.location); + const fragparts = parseQsFromFragment(window.location); - // don't try to redirect to the native apps if we're - // verifying a 3pid (but after we've loaded the config) - // or if the user is following a deep link - // (https://github.com/vector-im/riot-web/issues/7378) - const preventRedirect = fragparts.params.client_secret || fragparts.location.length > 0; + // don't try to redirect to the native apps if we're + // verifying a 3pid (but after we've loaded the config) + // or if the user is following a deep link + // (https://github.com/vector-im/riot-web/issues/7378) + const preventRedirect = fragparts.params.client_secret || fragparts.location.length > 0; - if (!preventRedirect) { - const isIos = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; - const isAndroid = /Android/.test(navigator.userAgent); - if (isIos || isAndroid) { - if (document.cookie.indexOf("riot_mobile_redirect_to_guide=false") === -1) { - window.location.href = "mobile_guide/"; - return; + if (!preventRedirect) { + const isIos = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; + const isAndroid = /Android/.test(navigator.userAgent); + if (isIos || isAndroid) { + if (document.cookie.indexOf("riot_mobile_redirect_to_guide=false") === -1) { + window.location.href = "mobile_guide/"; + return; + } } } + + const loadOlmPromise = loadOlm(); + // set the platform for react sdk + preparePlatform(); + // load config requires the platform to be ready + const loadConfigPromise = loadConfig(); + await settled(loadConfigPromise); // wait for it to settle + // keep initialising so that we can show any possible error with as many features (theme, i18n) as possible + + // Load language after loading config.json so that settingsDefaults.language can be applied + const loadLanguagePromise = loadLanguage(); + // as quickly as we possibly can, set a default theme... + const loadThemePromise = loadTheme(); + const loadSkinPromise = loadSkin(); + + // await things settling so that any errors we have to render have features like i18n running + await settled(loadSkinPromise, loadThemePromise, loadLanguagePromise); + + // ########################## + // error handling begins here + // ########################## + if (!acceptBrowser) { + await new Promise(resolve => { + console.error("Browser is missing required features."); + // take to a different landing page to AWOOOOOGA at the user + showIncompatibleBrowser(() => { + if (window.localStorage) { + window.localStorage.setItem('mx_accepts_unsupported_browser', String(true)); + } + console.log("User accepts the compatibility risks."); + resolve(); + }); + }); + } + + try { + // await config here + await loadConfigPromise; + } catch (error) { + // Now that we've loaded the theme (CSS), display the config syntax error if needed. + if (error.err && error.err instanceof SyntaxError) { + return showError(_t("Your Riot is misconfigured"), [ + _t("Your Riot configuration contains invalid JSON. Please correct the problem and reload the page."), + _t("The message from the parser is: %(message)s", { message: error.err.message || _t("Invalid JSON")}), + ]); + } + return showError(_t("Unable to load config file: please refresh the page to try again.")); + } + + // ################################## + // app load critical path starts here + // assert things started successfully + // ################################## + await rageshakePromise; + await loadOlmPromise; + await loadSkinPromise; + await loadThemePromise; + await loadLanguagePromise; + + // Finally, load the app. All of the other react-sdk imports are in this file which causes the skinner to + // run on the components. + await loadApp(fragparts.params); + } catch (err) { + console.error(err); + // Like the compatibility page, AWOOOOOGA at the user + await showError(_t("Your Riot is misconfigured"), [ + err.translatedMessage || _t("Unexpected error preparing the app. See console for details."), + ]); } - - await loadSkin(); - - let acceptBrowser = checkBrowserFeatures(); - if (!acceptBrowser && window.localStorage) { - acceptBrowser = Boolean(window.localStorage.getItem("mx_accepts_unsupported_browser")); - } - - // Finally, load the app. All of the other react-sdk imports are in this file which causes the skinner to - // run on the components. We use `require` here to make sure webpack doesn't optimize this into an async - // import and thus running before the skin can load. - await loadApp(fragparts.params, acceptBrowser); } -start(); +start().catch(err => { + console.error(err); + if (!acceptBrowser) { + // TODO redirect to static incompatible browser page + } +}); diff --git a/src/vector/init.ts b/src/vector/init.tsx similarity index 77% rename from src/vector/init.ts rename to src/vector/init.tsx index 8b8a62bb..c7c9141e 100644 --- a/src/vector/init.ts +++ b/src/vector/init.tsx @@ -21,6 +21,7 @@ limitations under the License. import olmWasmPath from "olm/olm.wasm"; import Olm from 'olm'; import * as ReactDOM from "react-dom"; +import * as React from "react"; import * as languageHandler from "matrix-react-sdk/src/languageHandler"; import SettingsStore from "matrix-react-sdk/src/settings/SettingsStore"; @@ -28,6 +29,7 @@ import ElectronPlatform from "./platform/ElectronPlatform"; import WebPlatform from "./platform/WebPlatform"; import PlatformPeg from "matrix-react-sdk/src/PlatformPeg"; import SdkConfig from "matrix-react-sdk/src/SdkConfig"; +import {setTheme} from "matrix-react-sdk/src/theme"; import { initRageshake } from "./rageshakesetup"; @@ -35,7 +37,7 @@ import { initRageshake } from "./rageshakesetup"; export const rageshakePromise = initRageshake(); export function preparePlatform() { - if ((window).ipcRenderer) { + if (window.ipcRenderer) { console.log("Using Electron platform"); const plaf = new ElectronPlatform(); PlatformPeg.set(plaf); @@ -45,21 +47,12 @@ export function preparePlatform() { } } -export async function loadConfig(): Promise { - const platform = PlatformPeg.get(); - - let configJson; - try { - configJson = await platform.getConfig(); - } catch (e) { - return e; - } finally { - // XXX: We call this twice, once here and once in MatrixChat as a prop. We call it here to ensure - // granular settings are loaded correctly and to avoid duplicating the override logic for the theme. - // - // Note: this isn't called twice for some wrappers, like the Jitsi wrapper. - SdkConfig.put(configJson || {}); - } +export async function loadConfig() { + // XXX: We call this twice, once here and once in MatrixChat as a prop. We call it here to ensure + // granular settings are loaded correctly and to avoid duplicating the override logic for the theme. + // + // Note: this isn't called twice for some wrappers, like the Jitsi wrapper. + SdkConfig.put(await PlatformPeg.get().getConfig() || {}); } export function loadOlm(): Promise { @@ -138,12 +131,36 @@ export async function loadSkin() { console.log("Skin loaded!"); } -export async function loadApp(fragParams: {}, acceptBrowser: boolean) { +export async function loadTheme() { + setTheme(); +} + +export async function loadApp(fragParams: {}) { // load app.js async so that its code is not executed immediately and we can catch any exceptions const module = await import( /* webpackChunkName: "riot-web-app" */ /* webpackPreload: true */ "./app"); - window.matrixChat = ReactDOM.render(await module.loadApp(fragParams, acceptBrowser), + window.matrixChat = ReactDOM.render(await module.loadApp(fragParams), document.getElementById('matrixchat')); } + +export async function showError(title: string, messages?: string[]) { + const ErrorView = (await import( + /* webpackChunkName: "error-view" */ + /* webpackPreload: true */ + "../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(, + document.getElementById('matrixchat')); +} + +export const _t = languageHandler._t;