diff --git a/package.json b/package.json index 22370b1706..fcb2b3e5d3 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "@babel/preset-typescript": "^7.7.4", "@babel/register": "^7.7.4", "@babel/runtime": "^7.7.6", + "@types/modernizr": "^3.5.3", "@types/react": "16.9", "@types/react-dom": "^16.9.4", "autoprefixer": "^9.7.3", diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 646fe6ea14..a19e20b5c3 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -14,9 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -interface Window { - Olm: { - init: () => Promise; - }; - mxSendRageshake: (text: string, withLogs?: boolean) => void; +import {ReactNode} from "react"; +import "modernizr"; + +declare global { + interface Window { + Modernizr: ModernizrAPI & FeatureDetects; + Olm: { + init: () => Promise; + }; + + mxSendRageshake: (text: string, withLogs?: boolean) => void; + matrixChat: ReactNode; + } + + // workaround for https://github.com/microsoft/TypeScript/issues/30933 + interface ObjectConstructor { + fromEntries?(xs: [string|number|symbol, any][]): object + } } diff --git a/src/vector/app.js b/src/vector/app.js index 131e1ca41b..836b2ef1d0 100644 --- a/src/vector/app.js +++ b/src/vector/app.js @@ -23,7 +23,6 @@ import React from 'react'; // access via the console global.React = React; -import ReactDOM from 'react-dom'; import * as sdk from 'matrix-react-sdk'; import PlatformPeg from 'matrix-react-sdk/src/PlatformPeg'; import * as VectorConferenceHandler from 'matrix-react-sdk/src/VectorConferenceHandler'; @@ -45,41 +44,6 @@ import {loadConfig, preparePlatform, loadLanguage, loadOlm} from "./init"; let lastLocationHashSet = null; -function checkBrowserFeatures() { - if (!window.Modernizr) { - console.error("Cannot check features - Modernizr global is missing."); - return false; - } - - // custom checks atop Modernizr because it doesn't have ES2018/ES2019 checks in it for some features we depend on, - // Modernizr requires rules to be lowercase with no punctuation: - // ES2018: http://www.ecma-international.org/ecma-262/9.0/#sec-promise.prototype.finally - window.Modernizr.addTest("promiseprototypefinally", () => - window.Promise && window.Promise.prototype && typeof window.Promise.prototype.finally === "function"); - // ES2019: http://www.ecma-international.org/ecma-262/10.0/#sec-object.fromentries - window.Modernizr.addTest("objectfromentries", () => - window.Object && typeof window.Object.fromEntries === "function"); - - const featureList = Object.keys(window.Modernizr); - - let featureComplete = true; - for (let i = 0; i < featureList.length; i++) { - if (window.Modernizr[featureList[i]] === undefined) { - console.error( - "Looked for feature '%s' but Modernizr has no results for this. " + - "Has it been configured correctly?", featureList[i], - ); - return false; - } - if (window.Modernizr[featureList[i]] === false) { - console.error("Browser missing feature: '%s'", featureList[i]); - // toggle flag rather than return early so we log all missing features rather than just the first. - featureComplete = false; - } - } - return featureComplete; -} - // Parse the given window.location and return parameters that can be used when calling // MatrixChat.showScreen(screen, params) function getScreenFromLocation(location) { @@ -164,7 +128,7 @@ function onTokenLoginCompleted() { window.location.href = formatted; } -export async function loadApp() { +export async function loadApp(fragParams: {}, acceptBrowser: boolean) { // 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; @@ -192,26 +156,8 @@ export async function loadApp() { // Load language after loading config.json so that settingsDefaults.language can be applied await loadLanguage(); - const fragparts = parseQsFromFragment(window.location); const params = parseQs(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; - - 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 = "mobile_guide/"; - return; - } - } - } - // as quickly as we possibly can, set a default theme... await setTheme(); @@ -235,45 +181,35 @@ export async function loadApp() { ); const GenericErrorPage = sdk.getComponent("structures.GenericErrorPage"); - window.matrixChat = ReactDOM.render( - , - document.getElementById('matrixchat'), - ); - return; + return ; } - const validBrowser = checkBrowserFeatures(); - - const acceptInvalidBrowser = window.localStorage && window.localStorage.getItem('mx_accepts_unsupported_browser'); - const urlWithoutQuery = window.location.protocol + '//' + window.location.host + window.location.pathname; console.log("Vector starting at " + urlWithoutQuery); if (configError) { - window.matrixChat = ReactDOM.render(
+ return
Unable to load config file: please refresh the page to try again. -
, document.getElementById('matrixchat')); - } else if (validBrowser || acceptInvalidBrowser) { +
; + } else if (acceptBrowser) { platform.startUpdater(); - // Don't bother loading the app until the config is verified - verifyServerConfig().then((newConfig) => { + try { + // Don't bother loading the app until the config is verified + const config = await verifyServerConfig(); const MatrixChat = sdk.getComponent('structures.MatrixChat'); - window.matrixChat = ReactDOM.render( - , - document.getElementById('matrixchat'), - ); - }).catch(err => { + return ; + } catch (err) { console.error(err); let errorMessage = err.translatedMessage @@ -282,23 +218,17 @@ export async function loadApp() { // Like the compatibility page, AWOOOOOGA at the user const GenericErrorPage = sdk.getComponent("structures.GenericErrorPage"); - window.matrixChat = ReactDOM.render( - , - document.getElementById('matrixchat'), - ); - }); + 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"); - window.matrixChat = ReactDOM.render( - , - document.getElementById('matrixchat'), - ); + return ; } } @@ -384,7 +314,6 @@ async function verifyServerConfig() { } } - validatedConfig.isDefault = true; // Just in case we ever have to debug this diff --git a/src/vector/index.js b/src/vector/index.js deleted file mode 100644 index ff31d7b62c..0000000000 --- a/src/vector/index.js +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2018, 2019 New Vector Ltd -Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2020 The Matrix.org Foundation C.I.C. - -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. -*/ - -// Require common CSS here; this will make webpack process it into bundle.css. -// Our own CSS (which is themed) is imported via separate webpack entry points -// in webpack.config.js -require('gfm.css/gfm.css'); -require('highlight.js/styles/github.css'); - -// These are things that can run before the skin loads - be careful not to reference the react-sdk though. -import './rageshakesetup'; -import './modernizr'; - -// load service worker if available on this platform -if ('serviceWorker' in navigator) { - navigator.serviceWorker.register('sw.js'); -} - -// Ensure the skin is the very first thing to load for the react-sdk. We don't even want to reference -// the SDK until we have to in imports. -console.log("Loading skin..."); -import * as sdk from 'matrix-react-sdk'; -import * as skin from "../component-index"; -sdk.loadSkin(skin); -console.log("Skin loaded!"); - -// 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. -require("./app").loadApp(); diff --git a/src/vector/index.ts b/src/vector/index.ts new file mode 100644 index 0000000000..d844827a85 --- /dev/null +++ b/src/vector/index.ts @@ -0,0 +1,122 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2018, 2019 New Vector Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2020 The Matrix.org Foundation C.I.C. + +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. +*/ + +// Require common CSS here; this will make webpack process it into bundle.css. +// Our own CSS (which is themed) is imported via separate webpack entry points +// in webpack.config.js +require('gfm.css/gfm.css'); +require('highlight.js/styles/github.css'); + +// These are things that can run before the skin loads - be careful not to reference the react-sdk though. +import {parseQsFromFragment} from "./url_utils"; +import './modernizr'; + +// load service worker if available on this platform +if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('sw.js'); +} + +async function settled(prom: Promise) { + try { + await prom; + } catch (e) { + console.error(e); + } +} + +function checkBrowserFeatures() { + if (!window.Modernizr) { + console.error("Cannot check features - Modernizr global is missing."); + return false; + } + + // custom checks atop Modernizr because it doesn't have ES2018/ES2019 checks in it for some features we depend on, + // Modernizr requires rules to be lowercase with no punctuation: + // ES2018: http://www.ecma-international.org/ecma-262/9.0/#sec-promise.prototype.finally + window.Modernizr.addTest("promiseprototypefinally", () => + window.Promise && window.Promise.prototype && typeof window.Promise.prototype.finally === "function"); + // ES2019: http://www.ecma-international.org/ecma-262/10.0/#sec-object.fromentries + window.Modernizr.addTest("objectfromentries", () => + window.Object && typeof window.Object.fromEntries === "function"); + + const featureList = Object.keys(window.Modernizr); + + let featureComplete = true; + for (let i = 0; i < featureList.length; i++) { + if (window.Modernizr[featureList[i]] === undefined) { + console.error( + "Looked for feature '%s' but Modernizr has no results for this. " + + "Has it been configured correctly?", featureList[i], + ); + return false; + } + if (window.Modernizr[featureList[i]] === false) { + console.error("Browser missing feature: '%s'", featureList[i]); + // toggle flag rather than return early so we log all missing features rather than just the first. + featureComplete = false; + } + } + return featureComplete; +} + +// 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` +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( + /* webpackChunkName: "init" */ + /* webpackPreload: true */ + "./init"); + + await settled(rageshakePromise); // give rageshake a chance to load/fail + + 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; + + 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; + } + } + } + + 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(); diff --git a/src/vector/init.ts b/src/vector/init.ts index 96745f53cc..8b8a62bb3c 100644 --- a/src/vector/init.ts +++ b/src/vector/init.ts @@ -20,14 +20,19 @@ limitations under the License. // @ts-ignore import olmWasmPath from "olm/olm.wasm"; import Olm from 'olm'; +import * as ReactDOM from "react-dom"; -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 ElectronPlatform from "./platform/ElectronPlatform"; 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 { initRageshake } from "./rageshakesetup"; + + +export const rageshakePromise = initRageshake(); export function preparePlatform() { if ((window).ipcRenderer) { @@ -112,3 +117,33 @@ export async function loadLanguage() { console.error("Unable to set language", e); } } + +export async function loadSkin() { + // Ensure the skin is the very first thing to load for the react-sdk. We don't even want to reference + // the SDK until we have to in imports. + console.log("Loading skin..."); + // load these async so that its code is not executed immediately and we can catch any exceptions + const [sdk, skin] = await Promise.all([ + import( + /* webpackChunkName: "matrix-react-sdk" */ + /* webpackPreload: true */ + "matrix-react-sdk"), + import( + /* webpackChunkName: "riot-web-component-index" */ + /* webpackPreload: true */ + // @ts-ignore - this module is generated so may fail lint + "../component-index"), + ]); + sdk.loadSkin(skin); + console.log("Skin loaded!"); +} + +export async function loadApp(fragParams: {}, acceptBrowser: boolean) { + // 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), + document.getElementById('matrixchat')); +} diff --git a/src/vector/rageshakesetup.ts b/src/vector/rageshakesetup.ts index 6445f4e95c..e495557429 100644 --- a/src/vector/rageshakesetup.ts +++ b/src/vector/rageshakesetup.ts @@ -30,8 +30,9 @@ import * as rageshake from "matrix-react-sdk/src/rageshake/rageshake"; import SdkConfig from "matrix-react-sdk/src/SdkConfig"; import sendBugReport from "matrix-react-sdk/src/rageshake/submit-rageshake"; -function initRageshake() { - rageshake.init().then(() => { +export function initRageshake() { + const prom = rageshake.init(); + prom.then(() => { console.log("Initialised rageshake."); console.log("To fix line numbers in Chrome: " + "Meatball menu → Settings → Blackboxing → Add /rageshake\\.js$"); @@ -46,10 +47,9 @@ function initRageshake() { }, (err) => { console.error("Failed to initialise rageshake: " + err); }); + return prom; } -initRageshake(); - window.mxSendRageshake = function(text: string, withLogs?: boolean) { if (withLogs === undefined) withLogs = true; if (!text || !text.trim()) { diff --git a/src/vector/url_utils.ts b/src/vector/url_utils.ts index d35de505ed..1e14fb5c86 100644 --- a/src/vector/url_utils.ts +++ b/src/vector/url_utils.ts @@ -32,7 +32,7 @@ export function parseQsFromFragment(location: Location) { const result = { location: decodeURIComponent(hashparts[0]), - params: {}, + params: {}, }; if (hashparts.length > 1) { diff --git a/webpack.config.js b/webpack.config.js index 9d8f333c87..6e84842d6d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -31,7 +31,7 @@ module.exports = (env, argv) => { ...development, entry: { - "bundle": "./src/vector/index.js", + "bundle": "./src/vector/index.ts", "indexeddb-worker": "./src/vector/indexeddb-worker.js", "mobileguide": "./src/vector/mobile_guide/index.js", "jitsi": "./src/vector/jitsi/index.ts", diff --git a/yarn.lock b/yarn.lock index ad7be626ed..a46162cf56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1245,6 +1245,11 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== +"@types/modernizr@^3.5.3": + version "3.5.3" + resolved "https://registry.yarnpkg.com/@types/modernizr/-/modernizr-3.5.3.tgz#8ef99e6252191c1d88647809109dc29884ba6d7a" + integrity sha512-jhMOZSS0UGYTS9pqvt6q3wtT3uvOSve5piTEmTMx3zzTuBLvSIMxSIBIc3d5lajVD5h4xc41AMZD2M5orN3PxA== + "@types/node@*": version "13.9.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.0.tgz#5b6ee7a77faacddd7de719017d0bc12f52f81589"