From 08aed0d21a8e7fd5e225140fa1ee8f0e879841c5 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 18:14:58 +0800 Subject: ... --- FrontEnd/package.json | 11 ++- FrontEnd/src/index.tsx | 1 + FrontEnd/src/service-worker.tsx | 104 +++++++++++++++++++++ FrontEnd/src/service-worker.txt | 104 --------------------- FrontEnd/src/sw.ts | 66 +++++++++++++ .../timeline-common/ConnectionStatusBadge.tsx | 2 +- FrontEnd/tsconfig.json | 5 +- FrontEnd/vite.config.js | 13 ++- 8 files changed, 194 insertions(+), 112 deletions(-) create mode 100644 FrontEnd/src/service-worker.tsx delete mode 100644 FrontEnd/src/service-worker.txt create mode 100644 FrontEnd/src/sw.ts diff --git a/FrontEnd/package.json b/FrontEnd/package.json index 38dd85ff..2ae7cae1 100644 --- a/FrontEnd/package.json +++ b/FrontEnd/package.json @@ -30,7 +30,13 @@ "regenerator-runtime": "^0.13.7", "remarkable": "^2.0.1", "rxjs": "^7.1.0", - "xregexp": "^5.0.2" + "xregexp": "^5.0.2", + "workbox-cacheable-response": "^6.1.5", + "workbox-expiration": "^6.1.2", + "workbox-precaching": "^6.1.0", + "workbox-routing": "^6.1.5", + "workbox-strategies": "^6.1.0", + "workbox-window": "^6.1.1" }, "scripts": { "start": "vite", @@ -60,6 +66,7 @@ "eslint-plugin-react-hooks": "^4.2.0", "prettier": "^2.3.1", "typescript": "^4.3.2", - "vite": "^2.3.7" + "vite": "^2.3.7", + "vite-plugin-pwa": "^0.8.1" } } diff --git a/FrontEnd/src/index.tsx b/FrontEnd/src/index.tsx index 83c25792..28034601 100644 --- a/FrontEnd/src/index.tsx +++ b/FrontEnd/src/index.tsx @@ -13,6 +13,7 @@ import "./index.css"; import "./i18n"; import "./palette"; +import "./service-worker"; import App from "./App"; diff --git a/FrontEnd/src/service-worker.tsx b/FrontEnd/src/service-worker.tsx new file mode 100644 index 00000000..ea8dfc32 --- /dev/null +++ b/FrontEnd/src/service-worker.tsx @@ -0,0 +1,104 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Button } from "react-bootstrap"; + +import { pushAlert } from "./services/alert"; + +if ("serviceWorker" in navigator) { + let isThisTriggerUpgrade = false; + + const upgradeSuccessLocalStorageKey = "TIMELINE_UPGRADE_SUCCESS"; + + if (window.localStorage.getItem(upgradeSuccessLocalStorageKey)) { + pushAlert({ + message: "serviceWorker.upgradeSuccess", + type: "success", + }); + window.localStorage.removeItem(upgradeSuccessLocalStorageKey); + } + + void import("workbox-window").then(({ Workbox, messageSW }) => { + const wb = new Workbox("/sw.js"); + let registration: ServiceWorkerRegistration | undefined; + + // externalactivated is not usable but I still use its name. + wb.addEventListener("controlling", () => { + const upgradeReload = (): void => { + window.localStorage.setItem(upgradeSuccessLocalStorageKey, "true"); + window.location.reload(); + }; + + if (isThisTriggerUpgrade) { + upgradeReload(); + } else { + const Message: React.FC = () => { + const { t } = useTranslation(); + return ( + <> + {t("serviceWorker.externalActivatedPrompt")} + + + ); + }; + + pushAlert({ + message: Message, + dismissTime: "never", + type: "warning", + }); + } + }); + + wb.addEventListener("activated", (event) => { + if (!event.isUpdate) { + pushAlert({ + message: "serviceWorker.availableOffline", + type: "success", + }); + } + }); + + // Add an event listener to detect when the registered + // service worker has installed but is waiting to activate. + wb.addEventListener("waiting", (): void => { + const upgrade = (): void => { + isThisTriggerUpgrade = true; + if (registration && registration.waiting) { + // Send a message to the waiting service worker, + // instructing it to activate. + // Note: for this to work, you have to add a message + // listener in your service worker. See below. + void messageSW(registration.waiting, { type: "SKIP_WAITING" }); + } + }; + + const UpgradeMessage: React.FC = () => { + const { t } = useTranslation(); + return ( + <> + {t("serviceWorker.upgradePrompt")} + + + ); + }; + + pushAlert({ + message: UpgradeMessage, + dismissTime: "never", + type: "success", + }); + }); + + void wb.register().then((reg) => { + registration = reg; + }); + }); +} diff --git a/FrontEnd/src/service-worker.txt b/FrontEnd/src/service-worker.txt deleted file mode 100644 index ea8dfc32..00000000 --- a/FrontEnd/src/service-worker.txt +++ /dev/null @@ -1,104 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { Button } from "react-bootstrap"; - -import { pushAlert } from "./services/alert"; - -if ("serviceWorker" in navigator) { - let isThisTriggerUpgrade = false; - - const upgradeSuccessLocalStorageKey = "TIMELINE_UPGRADE_SUCCESS"; - - if (window.localStorage.getItem(upgradeSuccessLocalStorageKey)) { - pushAlert({ - message: "serviceWorker.upgradeSuccess", - type: "success", - }); - window.localStorage.removeItem(upgradeSuccessLocalStorageKey); - } - - void import("workbox-window").then(({ Workbox, messageSW }) => { - const wb = new Workbox("/sw.js"); - let registration: ServiceWorkerRegistration | undefined; - - // externalactivated is not usable but I still use its name. - wb.addEventListener("controlling", () => { - const upgradeReload = (): void => { - window.localStorage.setItem(upgradeSuccessLocalStorageKey, "true"); - window.location.reload(); - }; - - if (isThisTriggerUpgrade) { - upgradeReload(); - } else { - const Message: React.FC = () => { - const { t } = useTranslation(); - return ( - <> - {t("serviceWorker.externalActivatedPrompt")} - - - ); - }; - - pushAlert({ - message: Message, - dismissTime: "never", - type: "warning", - }); - } - }); - - wb.addEventListener("activated", (event) => { - if (!event.isUpdate) { - pushAlert({ - message: "serviceWorker.availableOffline", - type: "success", - }); - } - }); - - // Add an event listener to detect when the registered - // service worker has installed but is waiting to activate. - wb.addEventListener("waiting", (): void => { - const upgrade = (): void => { - isThisTriggerUpgrade = true; - if (registration && registration.waiting) { - // Send a message to the waiting service worker, - // instructing it to activate. - // Note: for this to work, you have to add a message - // listener in your service worker. See below. - void messageSW(registration.waiting, { type: "SKIP_WAITING" }); - } - }; - - const UpgradeMessage: React.FC = () => { - const { t } = useTranslation(); - return ( - <> - {t("serviceWorker.upgradePrompt")} - - - ); - }; - - pushAlert({ - message: UpgradeMessage, - dismissTime: "never", - type: "success", - }); - }); - - void wb.register().then((reg) => { - registration = reg; - }); - }); -} diff --git a/FrontEnd/src/sw.ts b/FrontEnd/src/sw.ts new file mode 100644 index 00000000..0130e345 --- /dev/null +++ b/FrontEnd/src/sw.ts @@ -0,0 +1,66 @@ +/// +/// +/// + +import { precacheAndRoute, matchPrecache } from "workbox-precaching"; +import { registerRoute, setDefaultHandler } from "workbox-routing"; +import { + NetworkFirst, + NetworkOnly, + StaleWhileRevalidate, +} from "workbox-strategies"; +import { CacheableResponsePlugin } from "workbox-cacheable-response"; +import { ExpirationPlugin } from "workbox-expiration"; + +declare let self: ServiceWorkerGlobalScope; + +self.addEventListener("message", (event) => { + if (event.data && (event.data as { type: string }).type === "SKIP_WAITING") { + void self.skipWaiting(); + } +}); + +precacheAndRoute(self.__WB_MANIFEST); + +const networkOnly = new NetworkOnly(); + +registerRoute(new RegExp("/swagger/?.*"), new NetworkOnly()); + +registerRoute(new RegExp("/api/token/?.*"), new NetworkOnly()); +registerRoute(new RegExp("/api/search/?.*"), new NetworkOnly()); + +registerRoute( + new RegExp("/api/users/.+/avatar"), + new StaleWhileRevalidate({ + cacheName: "avatars", + plugins: [ + new CacheableResponsePlugin({ + statuses: [200], + }), + new ExpirationPlugin({ + maxAgeSeconds: 60 * 60 * 24 * 30 * 3, // 3 months + }), + ], + }) +); + +registerRoute( + new RegExp("/api/?.*"), + new NetworkFirst({ + plugins: [ + new CacheableResponsePlugin({ + statuses: [200], + }), + ], + }) +); + +setDefaultHandler((options) => { + const { request } = options; + + if (request instanceof Request && request.destination === "document") + return matchPrecache("/index.html").then((r) => + r == null ? Response.error() : r + ); + else return networkOnly.handle(options); +}); diff --git a/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx b/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx index 1b9d6d2a..df43d8d2 100644 --- a/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx +++ b/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx @@ -1,6 +1,6 @@ import React from "react"; import classnames from "classnames"; -import { HubConnectionState } from "srcmicrosoft/signalr"; +import { HubConnectionState } from "@microsoft/signalr"; import { useTranslation } from "react-i18next"; export interface ConnectionStatusBadgeProps { diff --git a/FrontEnd/tsconfig.json b/FrontEnd/tsconfig.json index 3afe2c3e..6b691e0e 100644 --- a/FrontEnd/tsconfig.json +++ b/FrontEnd/tsconfig.json @@ -2,9 +2,7 @@ "compilerOptions": { "target": "ESNext", "lib": ["DOM", "DOM.Iterable", "ESNext"], - "allowJs": false, - "skipLibCheck": false, - "esModuleInterop": false, + "skipLibCheck": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, @@ -15,7 +13,6 @@ "jsx": "react", "noEmit": true, "types": ["vite/client"], - "sourceMap": true, "baseUrl": "./", "paths": { "@/*": ["src/*"] diff --git a/FrontEnd/vite.config.js b/FrontEnd/vite.config.js index 2e85c36a..ee6f6931 100644 --- a/FrontEnd/vite.config.js +++ b/FrontEnd/vite.config.js @@ -3,10 +3,21 @@ */ import reactRefresh from "@vitejs/plugin-react-refresh"; +import { VitePWA } from "vite-plugin-pwa"; import { defineConfig } from "vite"; export default defineConfig({ - plugins: [reactRefresh()], + plugins: [ + reactRefresh(), + VitePWA({ + strategies: "injectManifest", + srcDir: "src", + filename: "sw.ts", + base: "/", + manifest: false, + includeAssets: "**", + }), + ], resolve: { alias: [{ find: "@", replacement: "/src" }], }, -- cgit v1.2.3