Skip to content

Commit 0090aa5

Browse files
committed
feat: Installable and offline support
1 parent 0c6cdd5 commit 0090aa5

File tree

5 files changed

+330
-0
lines changed

5 files changed

+330
-0
lines changed

src/403.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>403 - Forbidden</title>
6+
</head>
7+
<body>
8+
<h1>403 - Forbidden</h1>
9+
<p>Sorry, you do not have permission to access this page.</p>
10+
<script src="./dist/CoCreate.js"></script>
11+
</body>
12+
</html>

src/404.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>404 - Not Found</title>
6+
</head>
7+
<body>
8+
<h1>404 - Not Found</h1>
9+
<p>The page you are looking for could not be found.</p>
10+
<script src="./dist/CoCreate.js"></script>
11+
</body>
12+
</html>

src/manifest.webmanifest

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
{
2+
"short_name": "CoCreate",
3+
"name": "CoCreate",
4+
"description": "Build your business in miutes",
5+
"protocol_handlers": [
6+
{
7+
"protocol": "web+jngl",
8+
"url": "/lookup?type=%s"
9+
}
10+
],
11+
"orientation": "portrait-primary",
12+
"icons": [
13+
{
14+
"src": "./assets/icons/icon-72x72.png",
15+
"type": "image/png",
16+
"sizes": "72x72"
17+
},
18+
{
19+
"src": "./assets/icons/icon-96x96.png",
20+
"type": "image/png",
21+
"sizes": "96x96"
22+
},
23+
{
24+
"src": "./assets/icons/icon-128x128.png",
25+
"type": "image/png",
26+
"sizes": "128x128"
27+
},
28+
{
29+
"src": "./assets/icons/icon-144x144.png",
30+
"type": "image/png",
31+
"sizes": "144x144"
32+
},
33+
{
34+
"src": "./assets/icons/icon-152x152.png",
35+
"type": "image/png",
36+
"sizes": "152x152"
37+
},
38+
{
39+
"src": "./assets/icons/icon-192x192.png",
40+
"type": "image/png",
41+
"sizes": "192x192"
42+
},
43+
{
44+
"src": "./assets/icons/icon-384x384.png",
45+
"type": "image/png",
46+
"sizes": "384x384"
47+
},
48+
{
49+
"src": "./assets/icons/icon-512x512.png",
50+
"type": "image/png",
51+
"sizes": "512x512",
52+
"purpose": "maskable"
53+
}
54+
],
55+
"start_url": "/",
56+
"id": "/",
57+
"background_color": "whitesmoke",
58+
"display": "fullscreen",
59+
"theme_color": "#c7c7c7",
60+
"screenshots": [
61+
{
62+
"src": "./assets/screenshot-1.png",
63+
"type": "image/png",
64+
"sizes": "1903x950"
65+
}
66+
],
67+
"categories": "productivity",
68+
"file_handlers": [
69+
{
70+
"action": "/open-file",
71+
"accept": {
72+
"text/*": [
73+
".txt"
74+
]
75+
}
76+
}
77+
],
78+
"share_target": {
79+
"action": "/",
80+
"method": "GET",
81+
"enctype": "application/x-www-form-urlencoded",
82+
"params": {
83+
"title": "title",
84+
"text": "text",
85+
"url": "url"
86+
}
87+
}
88+
}

src/offline.html

Lines changed: 34 additions & 0 deletions
Large diffs are not rendered by default.

src/service-worker.js

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/********************************************************************************
2+
* Copyright (C) 2023 CoCreate and Contributors.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Affero General Public License as published
6+
* by the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
********************************************************************************/
17+
18+
/**
19+
* Commercial Licensing Information:
20+
* For commercial use of this software without the copyleft provisions of the AGPLv3,
21+
* you must obtain a commercial license from CoCreate LLC.
22+
* For details, visit <https://cocreate.app/licenses/> or contact us at [email protected].
23+
*/
24+
25+
const cacheName = "dynamic-v2";
26+
let organization_id = ""
27+
let storage = true
28+
let returnedFromCache = {};
29+
30+
const queryString = self.location.search;
31+
const queryParams = new URLSearchParams(queryString);
32+
let cacheType = queryParams.get('cache');
33+
34+
self.addEventListener("install", (e) => {
35+
console.log('Service Worker Installing')
36+
self.skipWaiting();
37+
});
38+
39+
self.addEventListener("activate", async (e) => {
40+
e.waitUntil(clients.claim());
41+
});
42+
43+
self.addEventListener("fetch", async (e) => {
44+
if (!(e.request.url.indexOf('http') === 0) || e.request.method === 'POST') return;
45+
46+
e.respondWith(
47+
caches
48+
.match(e.request)
49+
.then(async (cacheResponse) => {
50+
if (!navigator.onLine || !!cacheResponse && cacheType !== 'false') {
51+
const organization = cacheResponse.headers.get('organization')
52+
const lastModified = cacheResponse.headers.get('last-modified')
53+
54+
returnedFromCache[e.request.url] = { organization, lastModified }
55+
return cacheResponse;
56+
} else {
57+
const networkResponse = await fetch(e.request);
58+
59+
if (!organization_id)
60+
organization_id = networkResponse.headers.get('organization')
61+
62+
let storageHeader = networkResponse.headers.get('storage')
63+
if (storageHeader)
64+
storage = storageHeader
65+
66+
if (cacheType && cacheType !== 'false') {
67+
console.log('caching')
68+
69+
caches.open(cacheName).then((cache) => {
70+
if (networkResponse.status !== 206 && networkResponse.status !== 502) {
71+
const networkModified = networkResponse.headers.get('last-modified');
72+
// if (!networkModified) {
73+
// networkResponse.headers.set('Last-Modified', new Date().toISOString());
74+
// }
75+
cache.put(e.request, networkResponse);
76+
if (cacheType === 'reload' || cacheType === 'prompt') {
77+
const cacheModified = cacheResponse.headers.get('last-modified');
78+
if (networkModified !== cacheModified) {
79+
self.clients.matchAll().then((clients) => {
80+
clients.forEach((client) => {
81+
client.postMessage({ action: 'cacheType', cacheType }); // Send a custom message
82+
console.log(`file ${cacheType} has been triggered`)
83+
});
84+
});
85+
}
86+
}
87+
}
88+
}).catch(() => {
89+
90+
});
91+
}
92+
93+
if (!cacheResponse || cacheType === 'false' || cacheType === 'offline') {
94+
return networkResponse.clone();
95+
}
96+
97+
}
98+
99+
if (!!cacheResponse && cacheType !== 'false' && cacheType !== 'offline') {
100+
return cacheResponse;
101+
}
102+
103+
})
104+
.catch(function () {
105+
return caches.match('./offline.html');
106+
})
107+
);
108+
});
109+
110+
self.addEventListener('message', function (event) {
111+
if (event.data.action === 'getOrganization')
112+
event.source.postMessage({ action: 'getOrganization', organization_id });
113+
else if (event.data.action === 'checkCache') {
114+
event.source.postMessage({ action: 'checkCache', returnedFromCache: { ...returnedFromCache } });
115+
returnedFromCache = {}
116+
}
117+
});
118+
119+
120+
self.addEventListener('push', (event) => {
121+
const pushData = event.data.json(); // Assuming the push payload is JSON data
122+
123+
// Process the push notification data as needed
124+
const { socketUrl, _id } = pushData;
125+
126+
// Establish a WebSocket connection
127+
const socket = new WebSocket(socketUrl);
128+
129+
// Handle WebSocket events
130+
socket.addEventListener('open', () => {
131+
// The WebSocket connection is open, send the message to the server
132+
socket.send(JSON.stringify({ method: 'read.object', array: 'files', object: { _id }, uid: '' }));
133+
});
134+
135+
socket.addEventListener('message', (event) => {
136+
// Handle messages received from the server over the WebSocket
137+
const serverData = JSON.parse(event.data);
138+
if (serverData.uid === uid) {
139+
// TODO: add file to cache and close socket
140+
}
141+
});
142+
143+
socket.addEventListener('close', () => {
144+
// Handle the WebSocket connection being closed
145+
console.log('WebSocket connection closed.');
146+
});
147+
148+
socket.addEventListener('error', (error) => {
149+
// Handle WebSocket errors
150+
console.error('WebSocket error:', error);
151+
});
152+
153+
// Ensure that the service worker stays active until the WebSocket connection is closed
154+
event.waitUntil(
155+
new Promise((resolve) => {
156+
socket.addEventListener('close', () => {
157+
resolve();
158+
});
159+
})
160+
);
161+
});
162+
163+
// self.addEventListener('backgroundfetchsuccess', (event) => {
164+
// const bgFetch = event.registration;
165+
166+
// event.waitUntil(async function() {
167+
// // Create/open a cache.
168+
// const cache = await caches.open(cacheName);
169+
// // Get all the records.
170+
// const records = await bgFetch.matchAll();
171+
// // Copy each request/response across.
172+
// const promises = records.map(async (record) => {
173+
// const response = await record.responseReady;
174+
// console.log('putting ')
175+
// await cache.put(record.request, response);
176+
// });
177+
178+
// // Wait for the copying to complete
179+
// await Promise.all(promises);
180+
181+
// // Update the progress notification.
182+
// // event.updateUI({ title: 'Episode 5 ready to listen!' });
183+
// }());
184+
// });

0 commit comments

Comments
 (0)