@yz-social/civildefense.io 4.5.4 → 4.5.6

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yz-social/civildefense.io",
3
3
  "description": "Browser app to safely share live sightings on a map.",
4
- "version": "4.5.4",
4
+ "version": "4.5.6",
5
5
  "keywords": [
6
6
  "map",
7
7
  "browser",
@@ -9,14 +9,15 @@
9
9
  ],
10
10
  "scripts": {
11
11
  "start": "npm stop; node ./server/app.js",
12
- "stop": "pkill yz.social"
12
+ "stop": "pkill yz.social",
13
+ "postinstall": "touch server/location.json; chmod a+w server/location.json"
13
14
  },
14
15
  "type": "module",
15
16
  "exports": {
16
17
  ".": "./index.js"
17
18
  },
18
19
  "dependencies": {
19
- "@axona/protocol": "github:axona-net/axona-protocol#semver:^4.22.1",
20
+ "@axona/protocol": "github:axona-net/axona-protocol#5bf6f888cbbb5a58b9700c11582a4b6ca4c3141e",
20
21
  "cors": "^2.8.6",
21
22
  "express": "^4.22.1",
22
23
  "leaflet": "^1.9.4",
@@ -70,7 +70,7 @@ export class Agent {
70
70
  });
71
71
  }
72
72
  async setPublicData(data) { // Subscription to public data has fired. Update value, but do not not re-publish.
73
- let {payload, subject, type, topic, ts} = data; // fixme remove topic and ts
73
+ let {payload, tag, type, topic, ts} = data; // fixme remove topic and ts
74
74
  // WARNING: IF we chunkify avatars, and we use since:'all', then we need lock out asynchronous
75
75
  // decoding of later timestamps, or of null payloads.
76
76
  // if (payload && (type === 'avatar')) {
@@ -79,7 +79,7 @@ export class Agent {
79
79
  // payload = dataURL;
80
80
  // }
81
81
  this.updateValue(payload, 'public', type, false);
82
- if (subject) this.publicMsgId[type] = subject;
82
+ if (tag) this.publicMsgId[type] = tag;
83
83
  }
84
84
 
85
85
  static agents = {}; // tag => Agent
@@ -12,7 +12,7 @@ import { getContainingCells, findCoverCellsByCenterAndPoint } from './s2.js';
12
12
  const { localStorage, getComputedStyle, URL, URLSearchParams, domtoimage } = globalThis;
13
13
 
14
14
 
15
- export function getShareableURL(subject = null, tags = Hashtags.getSubscribe()) { // Answer a url that reflects application state.
15
+ export function getShareableURL(tag = null, tags = Hashtags.getSubscribe()) { // Answer a url that reflects application state.
16
16
  const params = new URLSearchParams(location.search);
17
17
  const zoom = map.getZoom();
18
18
  const { lat, lng } = map.getCenter();
@@ -21,7 +21,7 @@ export function getShareableURL(subject = null, tags = Hashtags.getSubscribe())
21
21
  if (lat !== null) params.set('lat', lat);
22
22
  if (lng !== null) params.set('lng', lng);
23
23
  if (zoom !== null) params.set('z', zoom);
24
- if (subject !== null) params.set('sub', subject);
24
+ if (tag !== null) params.set('alert', tag);
25
25
  return new URL(`?${params.toString()}`, location);
26
26
  }
27
27
  export async function share(properties) { // Invoke platform share API on properties.
@@ -61,14 +61,14 @@ export async function share(properties) { // Invoke platform share API on prope
61
61
 
62
62
  const ttl = 24 * 60 * 60e3; // 24 hours
63
63
  let openOnReceive = null;
64
- export function go({lat = null, lng = null, zoom = null, subject = null}) { // Go to specified location (if any) and open marker (if any).
64
+ export function go({lat = null, lng = null, zoom = null, alert = null}) { // Go to specified location (if any) and open marker (if any).
65
65
  if (lat !== null && lng !== null) {
66
66
  if (zoom) map.flyTo({lat, lng}, zoom);
67
67
  else map.flyTo({lat, lng});
68
68
  }
69
69
  openOnReceive = null;
70
- if (subject) {
71
- Alert.openPopup(subject) || (openOnReceive = subject);
70
+ if (alert) {
71
+ Alert.openPopup(alert) || (openOnReceive = alert);
72
72
  }
73
73
  }
74
74
 
@@ -77,9 +77,8 @@ let subscriptionsRegion;
77
77
  // We do not record exactly where you were looking across sessions, but we do record the containing level 9 cell.
78
78
  let lastLevel9Cell; // S2 level 9 cells average a radius of about 10km ~ 6.5 miles.
79
79
 
80
- let last = []; // Last published lat, lng, subject
80
+ let last = []; // Last published lat, lng, tag
81
81
  const maxPublish = 5;
82
- // Publish an alert to all applicable eventNames, canceling as required. Promises subject (msgId).
83
82
  let publishing = false;
84
83
 
85
84
  export class Alert extends Conversation { // A wrapper around L.marker
@@ -116,6 +115,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
116
115
  subscriptions = newKeys;
117
116
  subscriptionsRegion = region;
118
117
  }
118
+ // Publish an alert to all applicable eventNames, canceling as required. Promises tag (msgId).
119
119
  static async publish({lat, lng,
120
120
  originalPosting = undefined,
121
121
  hashtag = Hashtags.getPublish(),
@@ -129,7 +129,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
129
129
  // However, the 'unpublishing' (if any) is invoked first.
130
130
  // To do this, we must hash the eventName ourselves.
131
131
  //console.log('publish', {lat, lng, hashtag, payload, cancel, subject, issuedTime, rest});
132
- if (publishing) { console.log('skiping overlappying publish'); return; } // do not stack them up.
132
+ if (publishing) { console.log('skiping overlappying publish'); return null; } // do not stack them up.
133
133
  try {
134
134
  publishing = true;
135
135
 
@@ -154,7 +154,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
154
154
  for (const cell of oldCells) {
155
155
  const eventName = alertTopic(cell, hashtag);
156
156
  // Note: we cannot unpublish replies by others, but they expire after a while anyway.
157
- await contact.publish({eventName, region, subject, payload: null});
157
+ await contact.publish({eventName, region, killTag: subject, payload: null});
158
158
  throttleMS && await P2PWebNetwork.delay(throttleMS);
159
159
  }
160
160
  }
@@ -172,7 +172,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
172
172
  lastFillIn = null;
173
173
  }
174
174
  } else {
175
- await contact.publish({eventName, region, subject, payload: null});
175
+ await contact.publish({eventName, region, killTag: subject, payload: null});
176
176
  throttleMS && await P2PWebNetwork.delay(throttleMS);
177
177
  }
178
178
  }
@@ -191,8 +191,8 @@ export class Alert extends Conversation { // A wrapper around L.marker
191
191
  static closePopup() { // Close any open popup.
192
192
  map.closePopup();
193
193
  }
194
- static openPopup(subject) { // Open the marker specified by subject.
195
- const wrapper = this.getItem(subject);
194
+ static openPopup(alertTag) { // Open the marker specified by subject.
195
+ const wrapper = this.getItem(alertTag);
196
196
  wrapper?.openPopup();
197
197
  }
198
198
  async openPopup() { // Open this wrapper's popup, and resolve any waiting promise.
@@ -230,8 +230,8 @@ export class Alert extends Conversation { // A wrapper around L.marker
230
230
  wrapper.initChangeHashtag(popupAttribution);
231
231
  });
232
232
  }
233
- static ensure({subject, topic, ts, issuedTime, ...rest}) { // Add marker at position with appropriate fade if not already present.
234
- const alert = super.ensure({tag: subject, subject, issuedTime, ...rest}); // Does not include topic or ts. fixme: get rid of subject. fixme: why is ts different?
233
+ static ensure({tag, topic, ts, issuedTime, ...rest}) { // Add marker at position with appropriate fade if not already present.
234
+ const alert = super.ensure({tag, subject: tag, issuedTime, ...rest}); // Does not include topic or ts. fixme: get rid of subject. fixme: why is ts different?
235
235
  if (!alert) return null;
236
236
  // Regardless of initialize vs update, reset fader.
237
237
  const now = Date.now(),
@@ -242,7 +242,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
242
242
  alert.destroyer = setTimeout(() => alert.destroy(), remaining);
243
243
  return alert;
244
244
  }
245
- initialize({payload, hashtag, subject, agent, issuedTime, ...rest}) { // Set up the marker.
245
+ initialize({payload, hashtag, subject, agent, issuedTime, ...rest}) { // Set up the marker for a newly received alert.
246
246
  if (!payload) return null; // Do not cache. E.g., Received a delete event without the initial creation.
247
247
  const icon = this.constructor.makeIcon(hashtag);
248
248
  const {lat, lng, originalPosting} = payload;
@@ -261,7 +261,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
261
261
  networkPromise.then(async contact => {
262
262
  contact.subscribe({eventName: subject, region, handler: data => this.ensure(data)});
263
263
  });
264
- this.showNotification({tag: subject, agent, issuedTime});
264
+ this.showNotification({agent, issuedTime});
265
265
  return this;
266
266
  }
267
267
 
@@ -409,9 +409,9 @@ export class Alert extends Conversation { // A wrapper around L.marker
409
409
 
410
410
  // Each reply is separately published by its author, and only they can modify/unpublish it.
411
411
  async ensure(data) { // Add or update reply for this marker.
412
- data.tag = data.subject; //fixme
412
+ data.subject = data.tag; //fixme
413
413
  const reply = super.ensure(data);
414
- if (reply) {
414
+ if (reply) { // TODO? Move to AlertReply.initialize()?
415
415
  const {agent, issuedTime, payload} = data;
416
416
  const {file} = payload;
417
417
  if (file) {
@@ -453,17 +453,18 @@ export class Alert extends Conversation { // A wrapper around L.marker
453
453
  deleteReply(replyElement) {
454
454
  resetInactivityTimer();
455
455
  const {region} = this;
456
- networkPromise.then(async contact => contact.publish({eventName: this.subject, region, subject: replyElement.dataset.subject, payload: null}));
456
+ networkPromise.then(async contact => contact.publish({eventName: this.subject, region, killTag: replyElement.dataset.subject, payload: null}));
457
457
  }
458
- showNotification({issuedTime = this.issuedTime, body = '', agent = this.agent, tag = this.subject, lat = this.lat, lng = this.lng, hashtag = this.hashtag}) {
458
+ showNotification({issuedTime = this.issuedTime, body = '', agent = this.agent, alert = this.subject, lat = this.lat, lng = this.lng, hashtag = this.hashtag}) {
459
459
  // Give OS notification that comes back to here, unless act is us.
460
- if (agent == Agent.tag || !notificationsAllowed()) return;
460
+ // All notifications on the same alert (e.g., the post and each reply) have the same tag, so OS can collapse them.
461
+ if (agent === Agent.tag || !notificationsAllowed()) return;
461
462
  navigator.serviceWorker.ready.then(registration => {
462
463
  const timestamp = issuedTime;
463
464
  const icon = new URL('./images/civil-defense-192.png', location.href).href;
464
- const url = getShareableURL(tag, [hashtag]).href; // For opening page when it has been closed.
465
+ const url = getShareableURL(alert, [hashtag]).href; // For opening page when it has been closed.
465
466
  const data = {lat, lng, url};
466
- const options = {icon, timestamp, tag, body, data};
467
+ const options = {icon, timestamp, tag: alert, body, data};
467
468
  console.log('showNotification', hashtag, options);
468
469
  registration.showNotification(hashtag, options);
469
470
  });
@@ -1,5 +1,4 @@
1
1
  // TODO:
2
- // - clarify common lifecycle management, including existing reply to not get re-initialized
3
2
  // - In Alert, get rid of subject (just use tag)
4
3
  // - key off 'deleted' instead of payload:null, and bring handling here
5
4
  // - Bring pub/sub here, customized for SpatialConversation subclass
@@ -20,7 +19,7 @@ export class Tagged { // Maintains cached existence within a (possibly instance-
20
19
  for (const key in properties) {
21
20
  const existing = this[key];
22
21
  const proposed = properties[key];
23
- if (JSON.stringify(existing) !== JSON.stringify(proposed)) throw new Error(`Cannot update ${key} ${existing} to ${proposed}.`);
22
+ if (JSON.stringify(existing) !== JSON.stringify(proposed)) throw new Error(`Cannot update '${key}' from ${existing} to ${proposed}.`);
24
23
  }
25
24
  return this;
26
25
  }
@@ -84,7 +84,7 @@ export const Hashtags = {
84
84
  localStorage.setItem('hashtags', JSON.stringify(this.hashtags));
85
85
  if (resetSubscriptions) {
86
86
  Alert.updateSubscriptions();
87
- Alert.items.forEach(wrapper => this.hashtags[wrapper.hashtag] || wrapper.destroy());
87
+ Object.values(Alert.items).forEach(wrapper => this.hashtags[wrapper.hashtag] || wrapper.destroy());
88
88
  }
89
89
  },
90
90
  chipset: document.body.querySelector('.watching-hashtags'), // Element containing the user's chips.
@@ -48,7 +48,7 @@ export function updateLocation(lat, lng, zoom, positionLabel) { // initMap if ne
48
48
  const tagsArray = tags?.split(',') || [];
49
49
  tagsArray.forEach(tag => Hashtags.add(decodeURIComponent(tag)));
50
50
  Hashtags.onchange({resetSubscriptions: false}); // Too early to subscribe, but will be done during initialization.
51
- go({lat: params.get('lat'), lng: params.get('lng'), zoom: params.get('z'), subject: params.get('sub')});
51
+ go({lat: params.get('lat'), lng: params.get('lng'), zoom: params.get('z'), alert: params.get('alert')});
52
52
  // We don't need the query parameters now. Get rid of them. They're annoying.
53
53
  const copy = new URL(location);
54
54
  const dht = copy.searchParams.get('dht');
@@ -2,7 +2,6 @@ import { v4 as uuidv4 } from 'uuid';
2
2
  import { createNodeIdentity, createAuthorIdentity, geoCellId, geoCellCenter, WIRE_VERSION, KERNEL_VERSION } from '@axona/protocol';
3
3
  import { stringToBytes, bytesToString, publishChunkedBytes, receiveChunkedBytes } from '@axona/protocol/std';
4
4
  import { connect } from '@axona/protocol/connect.js';
5
- globalThis.RTCPeerConnection ||= await import('node-datachannel/polyfill').then(ndc => ndc.RTCPeerConnection);
6
5
  const { BigInt, URL, File, pica } = globalThis;
7
6
 
8
7
  /* Example:
@@ -25,6 +24,7 @@ export class P2PWebNetwork {
25
24
  } = {}) {
26
25
  // Promise a ready-to-use network peer.
27
26
  region = await region;
27
+
28
28
  const { peer, nodeIdentity, transport, status, disconnect } = await connect({
29
29
  bridge: bridgeUrl,
30
30
  location: region,
@@ -163,10 +163,10 @@ export class P2PWebNetwork {
163
163
  const {message, deleted, msgId, signerPubkey, topic, ts} = envelope;
164
164
  this.debug('received', {msgId, topic, ts, signerPubkey, deleted, message});
165
165
  if (deleted) {
166
- handler({subject: msgId, payload: null, agent: signerPubkey, topic, ts}); // fixme remove topic, ts here and below.
166
+ handler({tag: msgId, payload: null, agent: signerPubkey, topic, ts}); // fixme remove topic, ts here and below.
167
167
  return;
168
168
  }
169
- handler({...message, agent: signerPubkey, subject: msgId, topic, ts});
169
+ handler({...message, agent: signerPubkey, tag: msgId, topic, ts});
170
170
  };
171
171
  await this.peer.sub(topic, callback, {since});
172
172
  } else {
@@ -174,17 +174,18 @@ export class P2PWebNetwork {
174
174
  }
175
175
  }
176
176
  static currentPublishIdentity = null;
177
- async publish({eventName, region, owner, signWith = this.constructor.currentPublishIdentity, issuedTime = Date.now(), subject, payload, ...rest}) {
177
+ async publish({eventName, region, owner, signWith = this.constructor.currentPublishIdentity, issuedTime = Date.now(), killTag, payload, ...rest}) {
178
178
  // Publish data to subscribers of eventName.
179
+ if (killTag && payload) throw new Error(`Specify killTag (${killTag}) or payload ($(JSON.stringify(payload)}), but not both.`);
179
180
  await this.attachment; // Get connected.
180
181
  const topic = {region, name: eventName};
181
182
  if (owner) topic.owner = owner;
182
183
  const options = {signWith};
183
- this.debug('published', {topic, subject, payload, issuedTime, rest, signWith});
184
+ this.debug('published', {topic, killTag, payload, issuedTime, rest, signWith});
184
185
  if (payload) return await this.peer.pub(topic, {issuedTime, payload, ...rest}, options);
185
186
  // The next would not normally happen, but until since:'latest' works, we need a way to send a null payload and have the handler delete the entry.
186
- if (!subject) return await this.peer.pub(topic, {issuedTime, payload, ...rest}, options);
187
- return await this.peer.kill(topic, subject, options);
187
+ if (!killTag) return await this.peer.pub(topic, {issuedTime, payload, ...rest}, options);
188
+ return await this.peer.kill(topic, killTag, options);
188
189
  }
189
190
 
190
191
  host() {
@@ -1,6 +1,6 @@
1
- import pkg from '../package.json' with {type: 'json'};
1
+ import pkg from '../../package.json' with {type: 'json'};
2
2
  export const appVersion = pkg.version; // Overall semver of app. Used in display, and for comparison by service worker.
3
- export const dataVersion = appVersion.split('.')[0]; // Compatability differentiator used below.
3
+ export const dataVersion = globalThis.process?.env.EVENT_VERSION || appVersion.split('.')[0]; // Compatability differentiator used below.
4
4
 
5
5
  export function stripLeadingEmoji(string) { // Return string without any leading emoji (which might be of varying
6
6
  // length) followed by an optional emoji break character and any whitespace.
@@ -208,10 +208,10 @@ self.addEventListener('notificationclick', event => {
208
208
  clients
209
209
  .matchAll({type: 'window', includeUncontrolled: true})
210
210
  .then(async clientList => {
211
- console.log('notification', {title, body, data, clientList});
211
+ console.log('notification', {title, body, tag, data, clientList});
212
212
  for (const client of clientList) {
213
213
  console.log('notification click found client');
214
- return client.focus().then(() => client.postMessage({method: 'go', params: {subject: tag, ...data}}));
214
+ return client.focus().then(() => client.postMessage({method: 'go', params: {alert: tag, ...data}}));
215
215
  }
216
216
  // Client has been closed. Open one.
217
217
  console.log('notification click opening client', data.url);
package/movie/camera.jpg DELETED
Binary file