diff --git a/.vscode/settings.json b/.vscode/settings.json index d2a2b46..070d63c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -27,11 +27,13 @@ "arraybuffer", "authorised", "behaviour", + "btns", "categorised", "chartjs", "clientside", "commithash", "commonmeta", + "compat", "cryptocurrency", "desaturate", "dont", diff --git a/public/img/firefox.svg b/public/img/firefox.svg new file mode 100644 index 0000000..600f53a --- /dev/null +++ b/public/img/firefox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/google-chrome.svg b/public/img/google-chrome.svg new file mode 100644 index 0000000..03c0a18 --- /dev/null +++ b/public/img/google-chrome.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/index.html b/public/index.html index 53c27fc..31dd925 100644 --- a/public/index.html +++ b/public/index.html @@ -66,11 +66,27 @@ +
-
+
Loading KristWeb...
+ + diff --git a/src/global/compat/CompatCheckModal.less b/src/global/compat/CompatCheckModal.less new file mode 100644 index 0000000..41a05a7 --- /dev/null +++ b/src/global/compat/CompatCheckModal.less @@ -0,0 +1,43 @@ +// Copyright (c) 2020-2021 Drew Lemmy +// This file is part of KristWeb 2 under AGPL-3.0. +// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt +@import (reference) "../../App.less"; + +.compat-check-modal { + .ant-modal-confirm-btns { + display: none; + } + + .browser-choices { + display: flex; + flex-direction: row; + + // Remove the offset from the confirm modal body padding + margin-left: -38px; + padding-top: @margin-sm; + + a { + flex: 1; + + display: flex; + flex-direction: column; + + padding: @margin-sm; + + color: @text-color; + text-align: center; + background: transparent; + border-radius: @border-radius-base; + transition: all @animation-duration-base ease; + + &:hover { + background: @kw-lighter; + } + + img { + width: 96px; + margin: 0 auto @margin-md auto; + } + } + } +} diff --git a/src/global/compat/CompatCheckModal.tsx b/src/global/compat/CompatCheckModal.tsx new file mode 100644 index 0000000..db77b0c --- /dev/null +++ b/src/global/compat/CompatCheckModal.tsx @@ -0,0 +1,69 @@ +// Copyright (c) 2020-2021 Drew Lemmy +// This file is part of KristWeb 2 under AGPL-3.0. +// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt +import { Modal, Typography } from "antd"; + +import { CompatCheck } from "."; + +import "./CompatCheckModal.less"; + +const { Text } = Typography; + +interface Props { + failedChecks: CompatCheck[]; +} + +function CompatCheckModalContent({ failedChecks }: Props): JSX.Element { + // Note that this modal is not translated, as `fetch` is one of the APIs that + // may be unavailable. + + return <> +

Your browser is missing features required by KristWeb. +
Please upgrade your web browser.

+ + {/* Missing feature list */} +

+ Missing feature{failedChecks.length > 1 && <>(s)}:  + {failedChecks.map((c, i, a) => ( + + {c.url + ? + {c.name} + + : {c.name}} + + {i < a.length - 1 && <>, } + + ))} +

+ +

Please upgrade to the latest version of one of these recommended browsers:

+ + {/* Browser choices */} +
+ + + Google Chrome + + + + + Mozilla Firefox + +
+ ; +} + +export function openCompatCheckModal(failedChecks: CompatCheck[]): void { + Modal.error({ + title: "Unsupported browser", + + width: 640, + className: "compat-check-modal", + + okButtonProps: { style: { display: "none" }}, + closable: false, + + content: + }); +} diff --git a/src/global/compat/index.ts b/src/global/compat/index.ts new file mode 100644 index 0000000..b4ed6ab --- /dev/null +++ b/src/global/compat/index.ts @@ -0,0 +1,61 @@ +// Copyright (c) 2020-2021 Drew Lemmy +// This file is part of KristWeb 2 under AGPL-3.0. +// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt +import { localStorageAvailable } from "./localStorage"; + +import { openCompatCheckModal } from "./CompatCheckModal"; + +import Debug from "debug"; +const debug = Debug("kristweb:compat-check"); + +export interface CompatCheck { + name: string; + url?: string; + check: () => Promise | boolean; +} + +const CHECKS: CompatCheck[] = [ + { name: "Local Storage", url: "https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage", + check: localStorageAvailable }, + { name: "IndexedDB", url: "https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API", + check: () => !!window.indexedDB }, + { name: "Fetch", url: "https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API", + check: () => !!window.fetch }, + { name: "Crypto", url: "https://developer.mozilla.org/en-US/docs/Web/API/Window/crypto", + check: () => !!window.crypto }, + { name: "SubtleCrypto", url: "https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto", + check: () => !!window.crypto && !!window.crypto.subtle }, + { name: "Broadcast Channel", url: "https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API", + check: () => !!BroadcastChannel }, + { name: "Web Workers", url: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers", + check: () => !!window.Worker } + // NOTE: Service workers are not checked here, because they are disabled in + // Firefox private browsing. +]; + +/** Checks if the browser has all the required APIs, and returns an array of + * failed compatibility checks. */ +async function runCompatChecks(): Promise { + const failed: CompatCheck[] = []; + for (const check of CHECKS) { + try { + if (!(await check.check())) + throw new Error("check returned false"); + } catch (err) { + debug("compatibility check %s failed", check.name); + console.error(err); + failed.push(check); + } + } + return failed; +} + +/** Runs the compatibility checks, displaying the "Unsupported browser" modal + * and throwing if any of them fail. */ +export async function compatCheck(): Promise { + const failedChecks = await runCompatChecks(); + if (failedChecks.length) { + openCompatCheckModal(failedChecks); + throw new Error("compat checks failed"); + } +} diff --git a/src/global/compat/localStorage.ts b/src/global/compat/localStorage.ts new file mode 100644 index 0000000..8745e47 --- /dev/null +++ b/src/global/compat/localStorage.ts @@ -0,0 +1,31 @@ +// Copyright (c) 2020-2021 Drew Lemmy +// This file is part of KristWeb 2 under AGPL-3.0. +// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt + +// Implementation sourced from MDN: +// https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#feature-detecting_localstorage +export function localStorageAvailable( + type: "localStorage" | "sessionStorage" = "localStorage" +): boolean { + let storage; + try { + storage = window[type]; + const x = "__storage_test__"; + storage.setItem(x, x); + storage.removeItem(x); + return true; + } catch(e) { + return e instanceof DOMException && ( + // everything except Firefox + e.code === 22 || + // Firefox + e.code === 1014 || + // test name field too, because code might not be present + // everything except Firefox + e.name === "QuotaExceededError" || + // Firefox + e.name === "NS_ERROR_DOM_QUOTA_REACHED") && + // acknowledge QuotaExceededError only if there's something already stored + (!!storage && storage.length !== 0); + } +} diff --git a/src/index.tsx b/src/index.tsx index 233b61e..6e099b7 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,24 +7,34 @@ import ReactDOM from "react-dom"; +import { compatCheck } from "@global/compat"; +import { notification } from "antd"; + import "./index.css"; import App from "@app"; import Debug from "debug"; const debug = Debug("kristweb:index"); -// import reportWebVitals from "./reportWebVitals"; +async function main() { + debug("=========================== APP STARTING ==========================="); + debug("performing compat checks"); + await compatCheck(); -debug("============================ APP STARTING ============================"); -if (isLocalhost && !localStorage.getItem("status")) { - // Automatically enable debug logging on localhost - localStorage.setItem("debug", "kristweb:*"); - localStorage.setItem("status", "LOCAL"); - location.reload(); + if (isLocalhost && !localStorage.getItem("status")) { + // Automatically enable debug logging on localhost + localStorage.setItem("debug", "kristweb:*"); + localStorage.setItem("status", "LOCAL"); + location.reload(); + } + + debug("waiting for i18n"); + await i18nLoader; + + initialRender(); } -debug("waiting for i18n"); -i18nLoader.then(() => { +function initialRender() { debug("performing initial render"); ReactDOM.render( // FIXME: ant-design still has a few incompatibilities with StrictMode, most @@ -36,9 +46,20 @@ // , document.getElementById("root") ); -}); +} -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -// reportWebVitals(); +main().catch(err => { + // Remove the preloader + document.getElementById("kw-preloader")?.remove(); + + // Don't show the notification if a modal is already being shown + if (err?.message === "compat checks failed") return; + + debug("critical error in index.tsx"); + console.error(err); + + notification.error({ + message: "Critical error", + description: "A critical startup error has occurred. Please report this on GitHub. See console for details." + }); +});