Implement service worker using vite-plugin-pwa
parent
f9122c5227
commit
36c1cb47c0
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()),
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
|
@ -1 +1,2 @@
|
|||
/// <reference types="vite-plugin-comlink/client" />
|
||||
/// <reference types="vite-plugin-pwa/client" />
|
|
@ -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],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue