Implement service worker using vite-plugin-pwa

main^2
ZauberNerd 2023-07-10 00:42:06 +02:00
parent f9122c5227
commit 36c1cb47c0
No known key found for this signature in database
GPG Key ID: 9B617FBFF79E4F60
11 changed files with 132 additions and 295 deletions

View File

@ -3,33 +3,24 @@
<head>
<meta charset="utf-8" />
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
<link rel="preconnect" crossorigin="anonymous" href="https://fonts.googleapis.com">
<link rel="preconnect" crossorigin="anonymous" href="https://cdnjs.cloudflare.com">
<link rel="preconnect" crossorigin="anonymous" href="https://cdn.jsdelivr.net">
<link rel="icon" href="/favicon.ico" />
<!-- mobile app viewport -->
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="An open source ChatGPT app with a voice." />
<link rel="apple-touch-icon" href="/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="/manifest.json" />
<!--
Notice the use of in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Chat with GPT | Unofficial ChatGPT app</title>
<link rel="stylesheet" media="all" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.2/css/all.min.css" />
<link rel="stylesheet" media="all" href="https://fonts.googleapis.com/css?family=Open+Sans:100,400,300,500,700,800" />
<link rel="stylesheet" media="all" href="https://fonts.googleapis.com/css?family=Fira+Code:100,400,300,500,700,800" />
<link href="https://fonts.googleapis.com/css?family=Work+Sans:300,400,500,600,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css"
integrity="sha384-Xi8rHCmBmhbuyyhbI88391ZKP2dmfnOl4rT9ZfRI7mLTdk1wblIUnrIq35nqwEvC" crossorigin="anonymous">
<link rel="stylesheet" crossorigin="anonymous" media="all" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.2/css/all.min.css" />
<link rel="stylesheet" crossorigin="anonymous" media="all" href="https://fonts.googleapis.com/css?family=Open+Sans:100,400,300,500,700,800" />
<link rel="stylesheet" crossorigin="anonymous" media="all" href="https://fonts.googleapis.com/css?family=Fira+Code:100,400,300,500,700,800" />
<link rel="stylesheet" crossorigin="anonymous" href="https://fonts.googleapis.com/css?family=Work+Sans:300,400,500,600,700&display=swap">
<link rel="stylesheet" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css" integrity="sha384-Xi8rHCmBmhbuyyhbI88391ZKP2dmfnOl4rT9ZfRI7mLTdk1wblIUnrIq35nqwEvC">
<link rel="stylesheet" href="/prose.css" />
<link rel="canonical" href="https://www.chatwithgpt.ai" />
<style>

View File

@ -69,6 +69,8 @@
"@vitejs/plugin-react": "^4.0.2",
"babel-plugin-formatjs": "^10.5.3",
"typescript": "^4.9.5",
"vite": "^4.4.1"
"vite": "^4.4.1",
"vite-plugin-pwa": "^0.16.4",
"workbox-window": "^7.0.0"
}
}

View File

@ -1,21 +0,0 @@
{
"$schema": "https://json.schemastore.org/web-manifest-combined.json",
"short_name": "Chat with GPT",
"name": "Chat with GPT",
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff",
"icons": [
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
]
}

View File

@ -1,32 +1,37 @@
import { Button, Notification } from "@mantine/core";
import { useCallback } from "react";
import { useAppDispatch, useAppSelector } from "../store";
import { resetUpdate, selectUpdateAvailable } from "../store/pwa";
import { useRegisterSW } from "virtual:pwa-register/react";
export function InstallUpdateNotification() {
const updateAvailable = useAppSelector(selectUpdateAvailable);
const dispatch = useAppDispatch();
const {
offlineReady: [_, setOfflineReady],
needRefresh: [needRefresh, setNeedRefresh],
updateServiceWorker,
} = useRegisterSW({
onRegistered(r) {
console.log("SW Registered:", r);
},
onRegisterError(error) {
console.log("SW registration error", error);
},
});
const onClose = useCallback(() => dispatch(resetUpdate()), [dispatch]);
const onClose = () => {
setOfflineReady(false);
setNeedRefresh(false);
};
const onUpdate = useCallback(async () => {
dispatch(resetUpdate());
const onUpdate = useCallback(async () => {
updateServiceWorker(true);
}, []);
const registration = await navigator.serviceWorker.getRegistration();
if (registration && registration.waiting) {
registration.waiting.postMessage({ type: "SKIP_WAITING" });
}
window.location.reload();
}, [dispatch]);
return updateAvailable ? (
<Notification title="Update available!" onClose={onClose}>
Click{" "}
<Button compact onClick={onUpdate}>
Update now
</Button>{" "}
to get the latest version.
</Notification>
) : null;
return needRefresh ? (
<Notification title="Update available!" onClose={onClose}>
Click{" "}
<Button compact onClick={onUpdate}>
Update now
</Button>{" "}
to get the latest version.
</Notification>
) : null;
}

View File

@ -8,8 +8,6 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { PersistGate } from 'redux-persist/integration/react';
import { AppContextProvider } from './core/context';
import store, { persistor } from './store';
import * as serviceWorkerRegistration from "./serviceworker-registration";
import { setUpdateAvailable } from "./store/pwa";
import ChatPage from './components/pages/chat';
import LandingPage from './components/pages/landing';
@ -89,7 +87,3 @@ async function bootstrapApplication() {
}
bootstrapApplication();
serviceWorkerRegistration.register({
onUpdate: () => store.dispatch(setUpdateAvailable()),
});

View File

@ -1,43 +0,0 @@
/// <reference lib="webworker" />
/* eslint-disable no-restricted-globals */
// This service worker can be customized!
// See https://developer.chrome.com/docs/workbox/modules/
// for the list of available Workbox modules, or add any other
// code you'd like.
import { clientsClaim } from "workbox-core";
import { precacheAndRoute } from "workbox-precaching";
import { registerRoute } from "workbox-routing";
import { StaleWhileRevalidate } from "workbox-strategies";
declare const self: ServiceWorkerGlobalScope;
clientsClaim();
// Precache all of the assets generated by your build process.
precacheAndRoute(self.__WB_MANIFEST);
// An example runtime caching route for requests that aren't handled by the
// precache, in this case same-origin .png requests like those from in public/
registerRoute(
// Add in any other file extensions or routing criteria as needed.
({ url }) =>
url.origin === self.location.origin &&
(url.pathname.endsWith(".png") ||
url.pathname.endsWith(".ico") ||
url.pathname.endsWith(".json") ||
url.pathname.endsWith(".css")),
// Customize this strategy as needed, e.g., by changing to CacheFirst.
new StaleWhileRevalidate({
cacheName: "static",
})
);
// This allows the web app to trigger skipWaiting via
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting();
}
});

View File

@ -1,146 +0,0 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://cra.link/PWA
const isLocalhost = Boolean(
window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address.
window.location.hostname === "[::1]" ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
};
export function register(config?: Config) {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
"This web app is being served cache-first by a service " +
"worker. To learn more, visit https://cra.link/PWA"
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
"New content is available and will be used when all " +
"tabs for this page are closed. See https://cra.link/PWA."
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log("Content is cached for offline use.");
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch((error) => {
console.error("Error during service worker registration:", error);
});
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { "Service-Worker": "script" },
})
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get("content-type");
if (
response.status === 404 ||
(contentType != null && contentType.indexOf("javascript") === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
"No internet connection found. App is running in offline mode."
);
});
}
export function unregister() {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready
.then((registration) => {
registration.unregister();
})
.catch((error) => {
console.error(error.message);
});
}
}

View File

@ -15,7 +15,6 @@ import messageReducer from './message';
import settingsUIReducer from './settings-ui';
import sidebarReducer from './sidebar';
import uiReducer from './ui';
import pwaReducer from './pwa';
const persistConfig = {
key: 'root',
@ -38,7 +37,6 @@ const store = configureStore({
ui: uiReducer,
settingsUI: settingsUIReducer,
sidebar: persistReducer<ReturnType<typeof sidebarReducer>>(persistSidebarConfig, sidebarReducer),
pwa: pwaReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({

View File

@ -1,30 +0,0 @@
import { createSlice } from "@reduxjs/toolkit";
import type { RootState } from ".";
interface UpdateState {
updateAvailable: boolean;
}
const initialState: UpdateState = {
updateAvailable: false,
};
export const updateSlice = createSlice({
name: "pwa",
initialState,
reducers: {
setUpdateAvailable: (state) => {
state.updateAvailable = true;
},
resetUpdate: (state) => {
state.updateAvailable = false;
},
},
});
export const { setUpdateAvailable, resetUpdate } = updateSlice.actions;
export const selectUpdateAvailable = (state: RootState) =>
state.pwa.updateAvailable;
export default updateSlice.reducer;

View File

@ -1 +1,2 @@
/// <reference types="vite-plugin-comlink/client" />
/// <reference types="vite-plugin-pwa/client" />

View File

@ -1,5 +1,6 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa";
export default defineConfig(() => {
return {
@ -14,10 +15,11 @@ export default defineConfig(() => {
},
build: {
outDir: "build",
target: "es2020"
target: "es2020",
sourcemap: true,
},
esbuild: {
target: "es2020"
target: "es2020",
},
resolve: {
alias: {
@ -38,6 +40,90 @@ export default defineConfig(() => {
],
},
}),
VitePWA({
registerType: "autoUpdate",
includeAssets: ["favicon.ico", "lang/*.json"],
manifest: {
short_name: "Chat with GPT",
name: "Chat with GPT",
start_url: ".",
display: "standalone",
theme_color: "#000000",
background_color: "#ffffff",
icons: [
{
src: "logo192.png",
type: "image/png",
sizes: "192x192",
},
{
src: "logo512.png",
type: "image/png",
sizes: "512x512",
},
],
},
workbox: {
runtimeCaching: [
{
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
handler: "CacheFirst",
options: {
cacheName: "google-fonts-cache",
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365,
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
handler: "CacheFirst",
options: {
cacheName: "gstatic-fonts-cache",
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365,
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
urlPattern: /^https:\/\/cdnjs\.cloudflare\.com\/.*/i,
handler: "CacheFirst",
options: {
cacheName: "cloudflare-js-cdn",
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365,
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
urlPattern: /^https:\/\/cdn\.jsdelivr\.net\/.*/i,
handler: "CacheFirst",
options: {
cacheName: "jsdelivr-cdn",
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365,
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
],
},
}),
],
};
});