@yz-social/civildefense.io 4.5.29 → 5.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/index.js +1 -0
  2. package/package.json +4 -2
  3. package/public/about/add-topic-autocomplete.png +0 -0
  4. package/public/about/add-topic.png +0 -0
  5. package/public/about/can-happen.jpg +0 -0
  6. package/public/about/en.html +72 -30
  7. package/public/about/identity.html +48 -0
  8. package/public/about/index.html +1 -1
  9. package/public/about/join.webp +0 -0
  10. package/public/about/notification.html +71 -0
  11. package/public/about/posters.jpeg +0 -0
  12. package/public/about/pwa.html +61 -0
  13. package/public/about/style.css +15 -2
  14. package/public/about/template.html +28 -0
  15. package/public/about/topics.png +0 -0
  16. package/public/about/train-radio-button.png +0 -0
  17. package/public/about/train.png +0 -0
  18. package/public/about/ukraine-fires.jpeg +0 -0
  19. package/public/about/ukraine-fires.png +0 -0
  20. package/public/index.html +17 -9
  21. package/public/javascripts/alert.js +203 -119
  22. package/public/javascripts/browser-push.js +56 -0
  23. package/public/javascripts/conversation.js +6 -3
  24. package/public/javascripts/display.js +2 -1
  25. package/public/javascripts/hashtags.js +3 -1
  26. package/public/javascripts/main.js +102 -64
  27. package/public/javascripts/map.js +13 -6
  28. package/public/javascripts/p2pWebNetwork.js +37 -4
  29. package/public/javascripts/protocol.js +3 -3
  30. package/public/javascripts/pubsub.js +107 -98
  31. package/public/javascripts/push.js +28 -0
  32. package/public/javascripts/s2.js +24 -2
  33. package/public/javascripts/service-manager.js +11 -7
  34. package/public/javascripts/store-memory.js +74 -0
  35. package/public/javascripts/store-redis.js +101 -0
  36. package/public/javascripts/translations.js +6 -6
  37. package/public/service-worker.js +50 -5
  38. package/public/stylesheets/style.css +50 -17
  39. package/server/websocket.js +12 -5
@@ -1,6 +1,6 @@
1
1
  const { Request, Response, URL, clients} = self;
2
2
  // Little point in trying to automatically pull this through package.json, as we need a byte change in THIS file to trigger a new worker.
3
- const serviceVersion = '4.5.29';
3
+ const serviceVersion = '5.0.3';
4
4
 
5
5
  const cacheList = [ // The files we need.
6
6
  "/",
@@ -203,8 +203,46 @@ async function cacheSource(version, event) { // Cache source in the given versio
203
203
  return version;
204
204
  }
205
205
 
206
+ const issued = new Set();
207
+ function showNotification({lat, lng, issuedTime, hashtag, alert, body = '', force = false}) { // Promise to show a platform notification. There are two paths to here:
208
+ // 1. The app handles data from network connections, and determines that it should alert the user.
209
+ // In this case, the app sends the data to this service worker.
210
+ // 2. An upstream node would like to send the data over the network, but finds that we are not connected,
211
+ // and so pushes the the network envelope directly to the service worker.
212
+ // Abstractly, this function could be in either app or service worker, but the app might not be running
213
+ // in path 2, so it has to be here if we want to have the code in just one place.
214
+ // Any filtering (e.g., do not show notifications for one's own alerts) happens upstream of here. (We don't know the Agent.current and the sender isn't in the notification.)
215
+ const seenKey = alert + body; // If we click on a notification for an off-screen alert, we may process the alert again and try to notify again.
216
+ if (!force && issued.has(seenKey)) return null;
217
+ issued.add(seenKey);
218
+ const base = location.href;
219
+ const timestamp = issuedTime;
220
+ const icon = new URL('./images/civil-defense-192.png', base).href;
221
+ const queryString = `./?tags=${encodeURIComponent(hashtag)}&lat=${lat}&lng=${lng}&alert=${alert}`;
222
+ const url = new URL(queryString, base).href; // For opening page when it has been closed.
223
+ const data = {lat, lng, url};
224
+ const options = {icon, timestamp, body, data, tag: alert, renotify: true};
225
+ console.log('showNotification', {seenKey, hashtag, options});
226
+ return self.registration.showNotification(hashtag, options);
227
+ }
228
+
229
+ function showNotificationFromEnvelope({deleted, message, msgId}) { // We get the generic, application-independent envelope.
230
+ //console.log('envelope', message, msgId);
231
+ if (deleted) return cancelNotification(msgId);
232
+ const {issuedTime, hashtag, payload, alert = msgId} = message;
233
+ let {lat, lng, message:body, name = ''} = payload;
234
+ body ||= name;
235
+ return showNotification({alert, lat, lng, issuedTime, hashtag, body});
236
+ }
206
237
 
207
- self.addEventListener('message', async event => {
238
+ async function cancelNotification(tag, body = '') { // Cancel those that match tag, and if body, then only those also matching body.
239
+ for (const notification of await self.registration.getNotifications({tag})) {
240
+ console.log('cancel', {tag, body, notification});
241
+ if (!body || (body === notification.body)) notification.close();
242
+ }
243
+ }
244
+
245
+ self.addEventListener('message', event => {
208
246
  const {method, params} = event.data;
209
247
  switch (method) {
210
248
  case 'version':
@@ -214,11 +252,19 @@ self.addEventListener('message', async event => {
214
252
  event.waitUntil(cacheSource(params, event)
215
253
  .then(version => event.source.postMessage({method: 'cached', params: version})));
216
254
  break;
255
+ case 'notify':
256
+ event.waitUntil(showNotification(params));
257
+ break;
258
+ case 'cancelNotification':
259
+ event.waitUntil(cancelNotification(...params));
260
+ break;
217
261
  default:
218
262
  console.warn(`Unrecognized service worker message: "${event.data}".`);
219
263
  }
220
264
  });
221
265
 
266
+ self.addEventListener('push', event => event.waitUntil(showNotificationFromEnvelope(event.data.json())));
267
+
222
268
  self.addEventListener('notificationclick', event => {
223
269
  const {notification} = event;
224
270
  const {title, body, tag, data} = notification;
@@ -229,13 +275,12 @@ self.addEventListener('notificationclick', event => {
229
275
  clients
230
276
  .matchAll({type: 'window', includeUncontrolled: true})
231
277
  .then(async clientList => {
232
- console.log('notification', {title, body, tag, data, clientList});
233
278
  for (const client of clientList) {
234
- console.log('notification click found client');
279
+ console.log('notificationclick found client', notification, clientList);
235
280
  return client.focus().then(() => client.postMessage({method: 'go', params: {alert: tag, ...data}}));
236
281
  }
237
282
  // Client has been closed. Open one.
238
- console.log('notification click opening client', data.url);
283
+ console.log('notificationclick opening client', notification);
239
284
  return clients.openWindow(data.url);
240
285
  }),
241
286
  );
@@ -253,8 +253,8 @@ button, .leaflet-control-zoom > a, .leaflet-popup-close-button, md-outlined-icon
253
253
  }
254
254
  .alert-marker {
255
255
  text-shadow: 8px -3px 10px rgb(0 0 0 / 60%);
256
- animation: pulse ease 0.8s 1;
257
- transition: transform 0.8s, opacity 0.4s;
256
+ animation: pulse08 ease 0.8s 1;
257
+ transition: opacity 0.4s;
258
258
  }
259
259
  .alert-commented {
260
260
  display: none;
@@ -288,7 +288,7 @@ button, .leaflet-control-zoom > a, .leaflet-popup-close-button, md-outlined-icon
288
288
  left: -5px;
289
289
  background-image: radial-gradient(transparent 30%, var(--md-sys-color-primary), transparent 70%);
290
290
  transition: opacity 0.8s;
291
- animation: pulse ease 0.8s 1;
291
+ animation: pulse08 ease 0.8s 1;
292
292
  }
293
293
  .aggregate.alert-pin.starting {
294
294
  font-size: 0px;
@@ -296,19 +296,43 @@ button, .leaflet-control-zoom > a, .leaflet-popup-close-button, md-outlined-icon
296
296
  .aggregate.alert-pin.starting:after {
297
297
  opacity: 0;
298
298
  }
299
- @keyframes pulse {
300
- 0% { filter: saturate(1); }
301
- 25% { filter: saturate(4); }
302
- 50% { filter: saturate(1); }
303
- 75% { filter: saturate(0); }
304
- 100% { filter: saturate(1); }
305
- }
306
- @keyframes pulse2 { /* for restarting */
307
- 0% { filter: saturate(1); }
308
- 25% { filter: saturate(4); }
309
- 50% { filter: saturate(1); }
310
- 75% { filter: saturate(0); }
311
- 100% { filter: saturate(1); }
299
+ @keyframes pulse08 {
300
+ 0% { filter: saturate(1); background-color: rgb(189, 46, 47, 0%); border-radius: 8px; }
301
+ 25% { filter: saturate(4); background-color: rgb(189, 46, 47, 25%); }
302
+ 50% { filter: saturate(1); background-color: rgb(189, 46, 47, 40%); }
303
+ 75% { filter: saturate(0); background-color: rgb(189, 46, 47, 25%); }
304
+ 100% { filter: saturate(1); background-color: rgb(189, 46, 47, 0%); border-radius: 8px;}
305
+ }
306
+ /* 0.2s 0.2s */
307
+ /* <-0.8s-> <-0.8s-> */
308
+ /* 4 4 4 */
309
+ /* 1 1 1 1 1 1 */
310
+ /* 0 0 */
311
+ @keyframes pulse20 {
312
+ 0% { filter: saturate(1); background-color: rgb(189, 46, 47, 0%); border-radius: 8px; }
313
+ 10% { filter: saturate(4); background-color: rgb(189, 46, 47, 25%); }
314
+ 20% { filter: saturate(1); background-color: rgb(189, 46, 47, 50%); }
315
+ 30% { filter: saturate(0); }
316
+ 40% { filter: saturate(1); }
317
+ 50% { filter: saturate(4); }
318
+ 60% { filter: saturate(1); }
319
+ 70% { filter: saturate(0); }
320
+ 80% { filter: saturate(1); background-color: rgb(189, 46, 47, 50%); }
321
+ 90% { filter: saturate(4); background-color: rgb(189, 46, 47, 25%); }
322
+ 100% { filter: saturate(1); background-color: rgb(189, 46, 47, 0%); border-radius: 8px; }
323
+ }
324
+ @keyframes pulse20border {
325
+ 0% { filter: saturate(1); background-color: rgb(189, 46, 47, 0%); border-radius: 8px; border: solid thin rgb(189, 46, 47, 0);}
326
+ 10% { filter: saturate(4); background-color: rgb(189, 46, 47, 25%); border: solid thick rgb(189, 46, 47, 100%); }
327
+ 20% { filter: saturate(1); background-color: rgb(189, 46, 47, 50%); }
328
+ 30% { filter: saturate(0); }
329
+ 40% { filter: saturate(1); }
330
+ 50% { filter: saturate(4); }
331
+ 60% { filter: saturate(1); }
332
+ 70% { filter: saturate(0); }
333
+ 80% { filter: saturate(1); background-color: rgb(189, 46, 47, 50%); }
334
+ 90% { filter: saturate(4); background-color: rgb(189, 46, 47, 25%); border: solid thick rgb(189, 46, 47, 100%); }
335
+ 100% { filter: saturate(1); background-color: rgb(189, 46, 47, 0%); border-radius: 8px; border: solid thin rgb(189, 46, 47, 0); }
312
336
  }
313
337
 
314
338
  #checkForUpdates, #downloadUpdates, #updateContainer md-elevated-button {
@@ -320,7 +344,7 @@ button, .leaflet-control-zoom > a, .leaflet-popup-close-button, md-outlined-icon
320
344
  gap: 10px;
321
345
  }
322
346
  .pub {
323
- animation: pulse ease 0.8s 1;
347
+ animation: pulse08 ease 0.8s 1;
324
348
  --_label-text-weight: 800;
325
349
  --_selected-label-text-color: var(--md-sys-color-secondary);
326
350
  --md-filter-chip-selected-leading-icon-color: var(--md-sys-color-secondary);
@@ -627,6 +651,15 @@ input[type="file"] {
627
651
  padding: 0;
628
652
  min-width: unset;
629
653
  }
654
+ .pwa {
655
+ display: flex;
656
+ align-items: center;
657
+ gap: 5px;
658
+ }
659
+ #learnNotification:after, #learnIdentity:after {
660
+ content: "";
661
+ margin-right: 5px;
662
+ }
630
663
  .describe {
631
664
  margin-bottom: 5px;
632
665
  }
@@ -5,7 +5,7 @@ import * as operator from '../public/javascripts/pubsub.js';
5
5
  const sockets = {};
6
6
  operator.setReceiver((nodeId, id, envelope) => {
7
7
  const socket = sockets[nodeId];
8
- socket?.send(JSON.stringify([id, envelope]));
8
+ socket.send(JSON.stringify([id, envelope])); // We want an error if socket is gone, closed, etc.
9
9
  });
10
10
 
11
11
  function heartbeat() {
@@ -20,9 +20,13 @@ export function configureWebsocket(server) {
20
20
  sockets[nodeTag] = ws;
21
21
 
22
22
  ws.on('message', async message => {
23
- const [id, methodName, ...rest] = JSON.parse(message);
24
- const result = await operator[methodName](...rest);
25
- if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify([id, result]));
23
+ try {
24
+ const [id, methodName, ...rest] = JSON.parse(message);
25
+ const result = await operator[methodName](...rest);
26
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify([id, result]));
27
+ } catch (error) {
28
+ console.error(`Incomming ${error.message}: ${message}\n${error.stack}`);
29
+ }
26
30
  });
27
31
 
28
32
  ws.isAlive = true;
@@ -38,7 +42,10 @@ export function configureWebsocket(server) {
38
42
 
39
43
  const interval = setInterval(function ping() { // Keep-alive ping/pong on interval
40
44
  wss.clients.forEach(function each(ws) {
41
- if (!ws.isAlive) return ws.terminate();
45
+ if (!ws.isAlive) {
46
+ console.log('heartbeat failure');
47
+ return ws.terminate();
48
+ }
42
49
  if (ws.readyState == WebSocket.OPEN) {
43
50
  ws.isAlive = false;
44
51
  ws.ping();