Skip to content

Commit 4e5acfa

Browse files
authored
feat(service-worker): precaching and offline mode (#1149)
Co-authored-by: Andrejs <Leitans>
1 parent 5c1317f commit 4e5acfa

File tree

9 files changed

+1658
-68
lines changed

9 files changed

+1658
-68
lines changed

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@
138138
"webpack-bundle-analyzer": "^4.5.0",
139139
"webpack-cli": "^4.10.0",
140140
"webpack-dev-server": "^4.10.0",
141-
"webpack-merge": "^5.7.3"
141+
"webpack-merge": "^5.7.3",
142+
"workbox-webpack-plugin": "^7.1.0"
142143
},
143144
"dependencies": {
144145
"@apollo/client": "^3.9.4",
@@ -251,6 +252,11 @@
251252
"videostream": "^3.2.2",
252253
"web3": "1.2.4",
253254
"web3-utils": "^4.2.1",
255+
"workbox-core": "^7.1.0",
256+
"workbox-expiration": "^7.1.0",
257+
"workbox-precaching": "^7.1.0",
258+
"workbox-routing": "^7.1.0",
259+
"workbox-strategies": "^7.1.0",
254260
"worker-url": "^1.1.0"
255261
},
256262
"prec-commit": [

src/components/loader/loader.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,23 @@ const Bootloader = () => {
209209
};
210210

211211
function bootstrap() {
212+
if ('serviceWorker' in navigator) {
213+
console.log('Going to install service worker');
214+
window.addEventListener('load', () => {
215+
console.log('Starting to load service worker');
216+
navigator.serviceWorker
217+
.register('/service-worker.js')
218+
.then((registration) => {
219+
console.log('service worker registered: ', registration);
220+
})
221+
.catch((registrationError) => {
222+
console.log('service worker registration failed: ', registrationError);
223+
});
224+
});
225+
} else {
226+
console.log('No service worker is available');
227+
}
228+
212229
let _a;
213230
const assets =
214231
((_a =
@@ -229,9 +246,8 @@ function bootstrap() {
229246

230247
progressData.innerHTML = `Loading: <span>${Math.round(
231248
progress * 100
232-
)}%</span>. <br/> Network speed: <span>${
233-
Math.round(e.networkSpeed * 100) / 100
234-
} kbps</span>`;
249+
)}%</span>. <br/> Network speed: <span>${Math.round(e.networkSpeed * 100) / 100
250+
} kbps</span>`;
235251

236252
// console.log(e.loaded, e.loaded / e.totalSize); // @TODO
237253
})

src/containers/Search/SearchResults.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,32 @@
1-
import { useState, useEffect } from 'react';
2-
import { useParams } from 'react-router-dom';
1+
import { useEffect, useState } from 'react';
32
import InfiniteScroll from 'react-infinite-scroll-component';
4-
import { useDevice } from 'src/contexts/device';
5-
import { IpfsContentType } from 'src/services/ipfs/types';
3+
import { useParams } from 'react-router-dom';
4+
import Display from 'src/components/containerGradient/Display/Display';
65
import Spark from 'src/components/search/Spark/Spark';
76
import Loader2 from 'src/components/ui/Loader2';
8-
import { getIpfsHash } from 'src/utils/ipfs/helpers';
97
import { PATTERN_IPFS_HASH } from 'src/constants/patterns';
10-
import Display from 'src/components/containerGradient/Display/Display';
8+
import { useDevice } from 'src/contexts/device';
9+
import { IpfsContentType } from 'src/services/ipfs/types';
10+
import { getIpfsHash } from 'src/utils/ipfs/helpers';
1111

12+
import useIsOnline from 'src/hooks/useIsOnline';
1213
import { encodeSlash } from '../../utils/utils';
1314
import ActionBarContainer from './ActionBarContainer';
14-
import FirstItems from './_FirstItems.refactor';
15-
import useSearchData from './hooks/useSearchData';
16-
import { LinksTypeFilter, SortBy } from './types';
1715
import Filters from './Filters/Filters';
1816
import styles from './SearchResults.module.scss';
17+
import FirstItems from './_FirstItems.refactor';
1918
import { initialContentTypeFilterState } from './constants';
19+
import useSearchData from './hooks/useSearchData';
20+
import { LinksTypeFilter, SortBy } from './types';
2021

2122
const sortByLSKey = 'search-sort';
2223

2324
function SearchResults() {
2425
const { query: q, cid } = useParams();
26+
const isOnline = useIsOnline();
27+
const defaultMessage = isOnline
28+
? 'there are no answers or questions to this particle <br /> be the first and create one'
29+
: "ther's nothing to show, wait until you're online";
2530

2631
const query = q || cid || '';
2732

@@ -156,10 +161,7 @@ function SearchResults() {
156161
<p>{error.message}</p>
157162
</Display>
158163
) : (
159-
<Display color="white">
160-
there are no answers or questions to this particle <br /> be the
161-
first and create one
162-
</Display>
164+
<Display color="white">{defaultMessage}</Display>
163165
)}
164166
</div>
165167

src/containers/application/Header/SwitchAccount/SwitchAccount.tsx

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
1-
import React, { useMemo, useRef } from 'react';
21
import cx from 'classnames';
3-
import { Link, useLocation } from 'react-router-dom';
2+
import React, { useMemo, useRef } from 'react';
43
import { usePopperTooltip } from 'react-popper-tooltip';
4+
import { Link, useLocation } from 'react-router-dom';
55
import { Transition } from 'react-transition-group';
66

7+
import usePassportByAddress from 'src/features/passport/hooks/usePassportByAddress';
78
import useOnClickOutside from 'src/hooks/useOnClickOutside';
89
import { routes } from 'src/routes';
9-
import usePassportByAddress from 'src/features/passport/hooks/usePassportByAddress';
1010

11-
import { useAppSelector } from 'src/redux/hooks';
1211
import Pill from 'src/components/Pill/Pill';
12+
import { useBackend } from 'src/contexts/backend/backend';
1313
import { useSigningClient } from 'src/contexts/signerClient';
14+
import useIsOnline from 'src/hooks/useIsOnline';
15+
import { useAppSelector } from 'src/redux/hooks';
1416
import BroadcastChannelSender from 'src/services/backend/channels/BroadcastChannelSender';
15-
import { useBackend } from 'src/contexts/backend/backend';
16-
import { AvataImgIpfs } from '../../../portal/components/avataIpfs';
17-
import styles from './SwitchAccount.module.scss';
18-
import networkStyles from '../SwitchNetwork/SwitchNetwork.module.scss';
1917
import useMediaQuery from '../../../../hooks/useMediaQuery';
2018
import robot from '../../../../image/temple/robot.png';
19+
import { AvataImgIpfs } from '../../../portal/components/avataIpfs';
2120
import Karma from '../../Karma/Karma';
21+
import networkStyles from '../SwitchNetwork/SwitchNetwork.module.scss';
22+
import styles from './SwitchAccount.module.scss';
2223

2324
// should be refactored
2425
function AccountItem({
@@ -91,6 +92,7 @@ function AccountItem({
9192
function SwitchAccount() {
9293
const { signerReady } = useSigningClient();
9394
const { isIpfsInitialized } = useBackend();
95+
const isOnline = useIsOnline();
9496
const mediaQuery = useMediaQuery('(min-width: 768px)');
9597

9698
const [controlledVisible, setControlledVisible] = React.useState(false);
@@ -179,9 +181,10 @@ function SwitchAccount() {
179181
</button>
180182
)}
181183
{isReadOnly && <Pill color="yellow" text="read only" />}
182-
{!isReadOnly && !signerReady && (
184+
{!isReadOnly && !signerReady && isOnline && (
183185
<Pill color="red" text="switch keplr" />
184186
)}
187+
{!isOnline && <Pill color="red" text="offline" />}
185188
<Karma address={useGetAddress} />
186189
</div>
187190
)}
@@ -195,8 +198,10 @@ function SwitchAccount() {
195198
>
196199
<div
197200
className={cx(styles.containerAvatarConnect, {
198-
[styles.containerAvatarConnectFalse]: !isIpfsInitialized,
199-
[styles.containerAvatarConnectTrueGreen]: isIpfsInitialized,
201+
[styles.containerAvatarConnectFalse]:
202+
!isIpfsInitialized || !isOnline,
203+
[styles.containerAvatarConnectTrueGreen]:
204+
isIpfsInitialized && isOnline,
200205
})}
201206
>
202207
<AvataImgIpfs

src/hooks/useIsOnline.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useState, useEffect } from 'react';
2+
3+
function useIsOnline() {
4+
const [isOnline, setIsOnline] = useState(navigator.onLine);
5+
6+
useEffect(() => {
7+
function handleOnline() {
8+
console.log('isOnline', true);
9+
setIsOnline(true);
10+
}
11+
12+
function handleOffline() {
13+
console.log('isOnline', false);
14+
setIsOnline(false);
15+
}
16+
17+
window.addEventListener('online', handleOnline);
18+
window.addEventListener('offline', handleOffline);
19+
20+
return () => {
21+
window.removeEventListener('online', handleOnline);
22+
window.removeEventListener('offline', handleOffline);
23+
};
24+
}, []);
25+
26+
return isOnline;
27+
}
28+
29+
export default useIsOnline;

src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import DeviceProvider from './contexts/device';
4040
import IbcDenomProvider from './contexts/ibcDenom';
4141
import NetworksProvider from './contexts/networks';
4242
import BackendProvider from './contexts/backend/backend';
43+
4344
import AdviserProvider from './features/adviser/context';
4445
import HubProvider from './contexts/hub';
4546

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/* eslint-disable import/no-unused-modules */
2+
import { clientsClaim, setCacheNameDetails } from 'workbox-core';
3+
import { matchPrecache, precacheAndRoute } from 'workbox-precaching';
4+
import { registerRoute } from 'workbox-routing';
5+
import { CacheFirst, NetworkFirst } from 'workbox-strategies';
6+
import { ExpirationPlugin } from 'workbox-expiration';
7+
8+
declare const self: ServiceWorkerGlobalScope;
9+
10+
const prefix = 'cyb.ai';
11+
const suffix = 'v1';
12+
const precache = 'app-precache';
13+
14+
setCacheNameDetails({
15+
prefix,
16+
suffix,
17+
precache,
18+
});
19+
20+
self.skipWaiting();
21+
clientsClaim();
22+
precacheAndRoute(self.__WB_MANIFEST);
23+
24+
registerRoute(
25+
({ request }) =>
26+
request.destination === 'image' ||
27+
request.destination === 'style' ||
28+
request.destination === 'audio' ||
29+
request.destination === 'video' ||
30+
request.destination === 'font',
31+
new CacheFirst({
32+
cacheName: 'assets',
33+
plugins: [
34+
new ExpirationPlugin({
35+
maxAgeSeconds: 24 * 60 * 60,
36+
}),
37+
],
38+
})
39+
);
40+
41+
registerRoute(
42+
({ request }) => !navigator.onLine && request.mode === 'navigate',
43+
async ({ event }) => {
44+
const request = (event as any).request as Request;
45+
console.log('[Service worker] fecth document', request);
46+
47+
const cachedDocument = await matchPrecache('/index.html');
48+
49+
if (cachedDocument) {
50+
return cachedDocument;
51+
}
52+
53+
return new Response(
54+
`<body style="color: white; background-color: black; display: flex; justify-content: center; width: 100wv; height: 100hv; align-items: center">
55+
<div style="display: flex; flex-direction: column;">
56+
<div>We're sorry</div>
57+
<div>Cyb.ai is down</div>
58+
</div>
59+
</body>`,
60+
{
61+
headers: { 'Content-Type': 'text/html' },
62+
}
63+
);
64+
}
65+
);
66+
67+
registerRoute(
68+
({ request }) =>
69+
request.method === 'GET' &&
70+
request.destination !== 'document' &&
71+
!(
72+
request.destination === 'image' ||
73+
request.destination === 'style' ||
74+
request.destination === 'audio' ||
75+
request.destination === 'video' ||
76+
request.destination === 'font'
77+
),
78+
new NetworkFirst({ cacheName: 'api-responses' })
79+
);
80+
81+
function generateCacheKey(request: Request) {
82+
return request.url + JSON.stringify(request.body);
83+
}
84+
registerRoute(
85+
({ request }) => request.method === 'POST',
86+
async ({ event }) => {
87+
const request = (event as any).request as Request;
88+
const cacheKey = generateCacheKey(request);
89+
const cachedResponse = await caches.match(cacheKey);
90+
91+
if (cachedResponse) {
92+
return cachedResponse;
93+
}
94+
95+
const response = await fetch(request);
96+
97+
const cache = await caches.open('api-responses-post');
98+
await cache.put(cacheKey, response.clone());
99+
100+
return response;
101+
}
102+
);

webpack.config.common.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ const webpack = require('webpack');
44
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
55
const HTMLWebpackPlugin = require('html-webpack-plugin');
66
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
7-
const BootloaderPlugin = require('./src/components/loader/webpack-loader');
87
const WorkerUrlPlugin = require('worker-url/plugin');
8+
const WorkboxPlugin = require('workbox-webpack-plugin');
9+
const BootloaderPlugin = require('./src/components/loader/webpack-loader');
910

1011
require('dotenv').config();
1112

@@ -116,6 +117,11 @@ const config = {
116117
// ProvidePlugin configuration
117118
cyblog: ['src/utils/logging/cyblog.ts', 'default'],
118119
}),
120+
new WorkboxPlugin.InjectManifest({
121+
swSrc: 'src/services/service-worker/service-worker.ts',
122+
swDest: 'service-worker.js',
123+
maximumFileSizeToCacheInBytes: 50 * 1024 * 1024,
124+
}),
119125
],
120126
module: {
121127
rules: [

0 commit comments

Comments
 (0)