Merge pull request #13095 from vector-im/t3chguy/app_load2

App load order changes to catch errors better
This commit is contained in:
Michael Telatynski 2020-04-09 21:11:16 +01:00 committed by GitHub
commit c0b4ab9f67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 228 additions and 139 deletions

View File

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ReactNode} from "react";
import "modernizr"; import "modernizr";
import {Renderer} from "react-dom";
declare global { declare global {
interface Window { interface Window {
@ -25,7 +25,10 @@ declare global {
}; };
mxSendRageshake: (text: string, withLogs?: boolean) => void; mxSendRageshake: (text: string, withLogs?: boolean) => void;
matrixChat: ReactNode; matrixChat: ReturnType<Renderer>;
// electron-only
ipcRenderer: any;
} }
// workaround for https://github.com/microsoft/TypeScript/issues/30933 // workaround for https://github.com/microsoft/TypeScript/issues/30933

View File

@ -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<IProps> = ({title, messages}) => {
return <div className="mx_GenericErrorPage">
<div className="mx_GenericErrorPage_box">
<h1>{title}</h1>
<div>
{messages && messages.map(msg => <p key={msg}>
{ _t(msg) }
</p>)}
</div>
</div>
</div>;
};
ErrorView.propTypes = {
title: PropTypes.string.isRequired,
messages: PropTypes.arrayOf(PropTypes.string.isRequired),
};
export default ErrorView;

View File

@ -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.", "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", "The message from the parser is: %(message)s": "The message from the parser is: %(message)s",
"Invalid JSON": "Invalid JSON", "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.", "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", "Open user settings": "Open user settings",
"Riot Desktop on %(platformName)s": "Riot Desktop on %(platformName)s", "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", "Go to your browser to complete Sign In": "Go to your browser to complete Sign In",

View File

@ -26,7 +26,7 @@ global.React = React;
import * as sdk from 'matrix-react-sdk'; import * as sdk from 'matrix-react-sdk';
import PlatformPeg from 'matrix-react-sdk/src/PlatformPeg'; import PlatformPeg from 'matrix-react-sdk/src/PlatformPeg';
import * as VectorConferenceHandler from 'matrix-react-sdk/src/VectorConferenceHandler'; 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 AutoDiscoveryUtils from 'matrix-react-sdk/src/utils/AutoDiscoveryUtils';
import {AutoDiscovery} from "matrix-js-sdk/src/autodiscovery"; import {AutoDiscovery} from "matrix-js-sdk/src/autodiscovery";
import * as Lifecycle from "matrix-react-sdk/src/Lifecycle"; 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 {MatrixClientPeg} from 'matrix-react-sdk/src/MatrixClientPeg';
import SdkConfig from "matrix-react-sdk/src/SdkConfig"; 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 CallHandler from 'matrix-react-sdk/src/CallHandler';
import {loadConfig, preparePlatform, loadLanguage, loadOlm} from "./init";
let lastLocationHashSet = null; let lastLocationHashSet = null;
@ -128,7 +126,7 @@ function onTokenLoginCompleted() {
window.location.href = formatted; 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 // 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 // but alternatives seem to require changing the interface to passing Workers to js-sdk
const vectorIndexeddbWorkerScript = document.body.dataset.vectorIndexeddbWorkerScript; 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 // the bundling. The js-sdk will just fall back to accessing
// indexeddb directly with no worker script, but we want to // indexeddb directly with no worker script, but we want to
// make sure the indexeddb script is present, so fail hard. // 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); MatrixClientPeg.setIndexedDbWorkerScript(vectorIndexeddbWorkerScript);
CallHandler.setConferenceHandler(VectorConferenceHandler); CallHandler.setConferenceHandler(VectorConferenceHandler);
window.addEventListener('hashchange', onHashChange); window.addEventListener('hashchange', onHashChange);
await loadOlm();
// set the platform for react sdk
preparePlatform();
const platform = PlatformPeg.get(); 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); 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 = (
<div>
<p>
{_t(
"Your Riot configuration contains invalid JSON. Please correct the problem " +
"and reload the page.",
)}
</p>
<p>
{_t(
"The message from the parser is: %(message)s",
{message: configError.err.message || _t("Invalid JSON")},
)}
</p>
</div>
);
const GenericErrorPage = sdk.getComponent("structures.GenericErrorPage");
return <GenericErrorPage message={errorMessage} title={_t("Your Riot is misconfigured")} />;
}
const urlWithoutQuery = window.location.protocol + '//' + window.location.host + window.location.pathname; const urlWithoutQuery = window.location.protocol + '//' + window.location.host + window.location.pathname;
console.log("Vector starting at " + urlWithoutQuery); console.log("Vector starting at " + urlWithoutQuery);
if (configError) {
return <div className="error">
Unable to load config file: please refresh the page to try again.
</div>;
} else if (acceptBrowser) {
platform.startUpdater();
try { platform.startUpdater();
// Don't bother loading the app until the config is verified
const config = await verifyServerConfig();
const MatrixChat = sdk.getComponent('structures.MatrixChat');
return <MatrixChat
onNewScreen={onNewScreen}
makeRegistrationUrl={makeRegistrationUrl}
ConferenceHandler={VectorConferenceHandler}
config={config}
realQueryParams={params}
startingFragmentQueryParams={fragParams}
enableGuest={!config.disable_guests}
onTokenLoginCompleted={onTokenLoginCompleted}
initialScreenAfterLogin={getScreenFromLocation(window.location)}
defaultDeviceDisplayName={platform.getDefaultDeviceDisplayName()}
/>;
} catch (err) {
console.error(err);
let errorMessage = err.translatedMessage // Don't bother loading the app until the config is verified
|| _t("Unexpected error preparing the app. See console for details."); const config = await verifyServerConfig();
errorMessage = <span>{errorMessage}</span>; const MatrixChat = sdk.getComponent('structures.MatrixChat');
return <MatrixChat
// Like the compatibility page, AWOOOOOGA at the user onNewScreen={onNewScreen}
const GenericErrorPage = sdk.getComponent("structures.GenericErrorPage"); makeRegistrationUrl={makeRegistrationUrl}
return <GenericErrorPage message={errorMessage} title={_t("Your Riot is misconfigured")} />; ConferenceHandler={VectorConferenceHandler}
} config={config}
} else { realQueryParams={params}
console.error("Browser is missing required features."); startingFragmentQueryParams={fragParams}
// take to a different landing page to AWOOOOOGA at the user enableGuest={!config.disable_guests}
const CompatibilityPage = sdk.getComponent("structures.CompatibilityPage"); onTokenLoginCompleted={onTokenLoginCompleted}
return <CompatibilityPage onAccept={function() { initialScreenAfterLogin={getScreenFromLocation(window.location)}
if (window.localStorage) window.localStorage.setItem('mx_accepts_unsupported_browser', true); defaultDeviceDisplayName={platform.getDefaultDeviceDisplayName()}
console.log("User accepts the compatibility risks."); />;
loadApp();
}} />;
}
} }
async function verifyServerConfig() { async function verifyServerConfig() {

View File

@ -33,11 +33,13 @@ if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js'); navigator.serviceWorker.register('sw.js');
} }
async function settled(prom: Promise<any>) { async function settled(...promises: Array<Promise<any>>) {
try { for (const prom of promises) {
await prom; try {
} catch (e) { await prom;
console.error(e); } catch (e) {
console.error(e);
}
} }
} }
@ -76,47 +78,130 @@ function checkBrowserFeatures() {
return featureComplete; 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 // 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. // if modernizr fails we may not have a functional react to show the error message.
// try in react but fallback to an `alert` // 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() { async function start() {
// load init.ts async so that its code is not executed immediately and we can catch any exceptions // 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" */ /* webpackChunkName: "init" */
/* webpackPreload: true */ /* webpackPreload: true */
"./init"); "./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 // don't try to redirect to the native apps if we're
// verifying a 3pid (but after we've loaded the config) // verifying a 3pid (but after we've loaded the config)
// or if the user is following a deep link // or if the user is following a deep link
// (https://github.com/vector-im/riot-web/issues/7378) // (https://github.com/vector-im/riot-web/issues/7378)
const preventRedirect = fragparts.params.client_secret || fragparts.location.length > 0; const preventRedirect = fragparts.params.client_secret || fragparts.location.length > 0;
if (!preventRedirect) { if (!preventRedirect) {
const isIos = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; const isIos = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
const isAndroid = /Android/.test(navigator.userAgent); const isAndroid = /Android/.test(navigator.userAgent);
if (isIos || isAndroid) { if (isIos || isAndroid) {
if (document.cookie.indexOf("riot_mobile_redirect_to_guide=false") === -1) { if (document.cookie.indexOf("riot_mobile_redirect_to_guide=false") === -1) {
window.location.href = "mobile_guide/"; window.location.href = "mobile_guide/";
return; 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
}
});

View File

@ -21,6 +21,7 @@ limitations under the License.
import olmWasmPath from "olm/olm.wasm"; import olmWasmPath from "olm/olm.wasm";
import Olm from 'olm'; import Olm from 'olm';
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
import * as React from "react";
import * as languageHandler from "matrix-react-sdk/src/languageHandler"; import * as languageHandler from "matrix-react-sdk/src/languageHandler";
import SettingsStore from "matrix-react-sdk/src/settings/SettingsStore"; import SettingsStore from "matrix-react-sdk/src/settings/SettingsStore";
@ -28,6 +29,7 @@ import ElectronPlatform from "./platform/ElectronPlatform";
import WebPlatform from "./platform/WebPlatform"; import WebPlatform from "./platform/WebPlatform";
import PlatformPeg from "matrix-react-sdk/src/PlatformPeg"; import PlatformPeg from "matrix-react-sdk/src/PlatformPeg";
import SdkConfig from "matrix-react-sdk/src/SdkConfig"; import SdkConfig from "matrix-react-sdk/src/SdkConfig";
import {setTheme} from "matrix-react-sdk/src/theme";
import { initRageshake } from "./rageshakesetup"; import { initRageshake } from "./rageshakesetup";
@ -35,7 +37,7 @@ import { initRageshake } from "./rageshakesetup";
export const rageshakePromise = initRageshake(); export const rageshakePromise = initRageshake();
export function preparePlatform() { export function preparePlatform() {
if ((<any>window).ipcRenderer) { if (window.ipcRenderer) {
console.log("Using Electron platform"); console.log("Using Electron platform");
const plaf = new ElectronPlatform(); const plaf = new ElectronPlatform();
PlatformPeg.set(plaf); PlatformPeg.set(plaf);
@ -45,21 +47,12 @@ export function preparePlatform() {
} }
} }
export async function loadConfig(): Promise<Error | void> { export async function loadConfig() {
const platform = PlatformPeg.get(); // 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.
let configJson; //
try { // Note: this isn't called twice for some wrappers, like the Jitsi wrapper.
configJson = await platform.getConfig(); SdkConfig.put(await PlatformPeg.get().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 function loadOlm(): Promise<void> { export function loadOlm(): Promise<void> {
@ -138,12 +131,36 @@ export async function loadSkin() {
console.log("Skin loaded!"); 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 // load app.js async so that its code is not executed immediately and we can catch any exceptions
const module = await import( const module = await import(
/* webpackChunkName: "riot-web-app" */ /* webpackChunkName: "riot-web-app" */
/* webpackPreload: true */ /* webpackPreload: true */
"./app"); "./app");
window.matrixChat = ReactDOM.render(await module.loadApp(fragParams, acceptBrowser), window.matrixChat = ReactDOM.render(await module.loadApp(fragParams),
document.getElementById('matrixchat')); 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(<ErrorView title={title} messages={messages} />,
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(<CompatibilityPage onAccept={onAccept} />,
document.getElementById('matrixchat'));
}
export const _t = languageHandler._t;