diff --git a/package.json b/package.json index 22370b17..fcb2b3e5 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 90411e7b..a19e20b5 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -14,12 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -type ReactNode = import("react").ReactNode; +import {ReactNode} from "react"; +import "modernizr"; -interface Window { - Olm: { - init: () => Promise; - }; - mxSendRageshake: (text: string, withLogs?: boolean) => void; - matrixChat: ReactNode; +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 dcdc4769..836b2ef1 100644 --- a/src/vector/app.js +++ b/src/vector/app.js @@ -44,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) { @@ -163,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; @@ -191,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(); @@ -237,17 +184,13 @@ export async function loadApp() { 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) { return
Unable to load config file: please refresh the page to try again.
; - } else if (validBrowser || acceptInvalidBrowser) { + } else if (acceptBrowser) { platform.startUpdater(); try { @@ -260,7 +203,7 @@ export async function loadApp() { ConferenceHandler={VectorConferenceHandler} config={config} realQueryParams={params} - startingFragmentQueryParams={fragparts.params} + startingFragmentQueryParams={fragParams} enableGuest={!config.disable_guests} onTokenLoginCompleted={onTokenLoginCompleted} initialScreenAfterLogin={getScreenFromLocation(window.location)} diff --git a/src/vector/index.ts b/src/vector/index.ts index 74db1cd6..d844827a 100644 --- a/src/vector/index.ts +++ b/src/vector/index.ts @@ -25,6 +25,7 @@ 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 @@ -40,6 +41,41 @@ async function settled(prom: Promise) { } } +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` @@ -52,11 +88,35 @@ async function start() { 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(); + await loadApp(fragparts.params, acceptBrowser); } start(); diff --git a/src/vector/init.ts b/src/vector/init.ts index 57941e88..8b8a62bb 100644 --- a/src/vector/init.ts +++ b/src/vector/init.ts @@ -138,11 +138,12 @@ export async function loadSkin() { console.log("Skin loaded!"); } -export async function loadApp() { +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(), document.getElementById('matrixchat')); + window.matrixChat = ReactDOM.render(await module.loadApp(fragParams, acceptBrowser), + document.getElementById('matrixchat')); } diff --git a/src/vector/url_utils.ts b/src/vector/url_utils.ts index d35de505..1e14fb5c 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 9d8f333c..6e84842d 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 ad7be626..a46162cf 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"