@yz-social/civildefense.io 4.5.9 → 4.5.16

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/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { P2PWebNetwork } from "./public/javascripts/p2pWebNetwork.js";
2
- export { agentTopic, alertTopic, canonicalTag } from "./public/javascripts/versions.js";
2
+ export { topicRegion, agentTopic, alertTopic, canonicalTag } from "./public/javascripts/versions.js";
3
3
  export { getContainingCells } from "./public/javascripts/s2.js";
4
4
  export { location } from "./server/getLocation.js";
5
+ export { deriveTopicIdBig } from "@axona/protocol";
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.9",
4
+ "version": "4.5.16",
5
5
  "keywords": [
6
6
  "map",
7
7
  "browser",
@@ -10,14 +10,16 @@
10
10
  "scripts": {
11
11
  "start": "npm stop; node ./server/app.js",
12
12
  "stop": "pkill yz.social",
13
- "postinstall": "touch server/location.json; chmod a+w server/location.json"
13
+ "postinstall": "touch server/location.json; chmod a+w server/location.json",
14
+ "prepack": "mv server/location.json server/location.save.json",
15
+ "postpack": "mv server/location.save.json server/location.json"
14
16
  },
15
17
  "type": "module",
16
18
  "exports": {
17
19
  ".": "./index.js"
18
20
  },
19
21
  "dependencies": {
20
- "@axona/protocol": "github:axona-net/axona-protocol#semver:^4.48.0",
22
+ "@axona/protocol": "github:axona-net/axona-protocol#semver:^4.59.2",
21
23
  "cors": "^2.8.6",
22
24
  "express": "^4.22.1",
23
25
  "leaflet": "^1.9.4",
Binary file
package/public/index.html CHANGED
@@ -25,12 +25,11 @@
25
25
  "uuid": "https://unpkg.com/uuid@13.0.0/dist/index.js",
26
26
  "@material/web/": "https://esm.run/@material/web/",
27
27
  "s2js": "./s2js/s2js.esm.js",
28
- "bigfloat": "./bigfloat/esm/index.js",
29
- "leaflet": "./leaflet/leaflet-src.esm.js",
30
- "minidenticons": "./minidenticons/minidenticons.min.js",
31
- "@axona/protocol": "./axona-protocol/src/index.js",
32
- "@axona/protocol/std": "./axona-protocol/std/index.js",
33
- "@axona/protocol/connect.js": "./axona-protocol/src/connect.js"
28
+ "bigfloat": "./bigfloat/esm/index.js",
29
+ "leaflet": "./leaflet/leaflet-src.esm.js",
30
+ "minidenticons": "./minidenticons/minidenticons.min.js",
31
+ "@axona/protocol": "./axona-protocol/src/index.js",
32
+ "@axona/protocol/std": "./axona-protocol/std/index.js"
34
33
  }
35
34
  }
36
35
  </script>
@@ -1,7 +1,6 @@
1
1
  const { localStorage } = globalThis;
2
2
  import { v4 as uuidv4 } from 'uuid';
3
3
  import { minidenticonSvg } from 'minidenticons';
4
- import { createAuthorIdentity } from '@axona/protocol';
5
4
  import { agentTopic, agentPersistKey } from './versions.js';
6
5
  import { Int } from './translations.js';
7
6
  import { consume, openDisplay } from './display.js';
@@ -65,7 +64,7 @@ export class Agent {
65
64
  const owner = this.tag;
66
65
  ['handle', 'avatar'].forEach(type => {
67
66
  const eventName = this.networkPersistKey(type);
68
- contact.subscribe({eventName, region, owner, /*since: 'latest',*/ handler: data => this.setPublicData({...data, type})});
67
+ contact.subscribe({eventName, region, owner, since: 'latest', handler: data => this.setPublicData({...data, type})});
69
68
  });
70
69
  });
71
70
  }
@@ -129,23 +128,25 @@ export class Agent {
129
128
  const eventName = this.networkPersistKey(type);
130
129
  const region = this.currentRegion;
131
130
  const owner = this.tag;
132
- // TODO: set owner as well.
133
131
  const contact = await networkPromise;
134
- if (value) {
135
- let payload = value;
136
- // Our downsampling is such that we do not need to chunkify.
137
- // if (type === 'avatar') {
138
- // const blob = await P2PWebNetwork.dataURL2blob(value);
139
- // payload = (await contact.chunkifyBlob({blob, region})).topic;
140
- // }
141
- return contact.publish({eventName, region, owner, payload});
142
- }
143
- // For now, until since:'latest' works, supply a null payload and no subject.
144
- await contact.publish({eventName, region, owner, payload: null});
145
- // const subject = this.publicMsgId[type];
146
- // if (!subject) return null; // We have not published a value, so nothing to kill.
147
- // await contact.publish({eventName, region, owner, subject, payload: null});
148
- // return null;
132
+
133
+ // First kill previous, if any.
134
+ // Publish doesn't know whether subscribers will be since all or latest, so it must retain all unkilled.
135
+ // Thus if we only kill the last one when there is no value,
136
+ // a subscribe since:latest will produce the PREVIOUS value -- the last unkilled one.
137
+
138
+ const killTag = this.publicMsgId[type];
139
+ console.log('persist', {type, killTag, value: value && value.slice(0, 15)});
140
+ if (killTag) await contact.publish({eventName, region, owner, killTag, payload: null});
141
+
142
+ if (!value) return null;
143
+ let payload = value;
144
+ // Our downsampling is such that we do not need to chunkify.
145
+ // if (type === 'avatar') {
146
+ // const blob = await P2PWebNetwork.dataURL2blob(value);
147
+ // payload = (await contact.chunkifyBlob({blob, region})).topic;
148
+ // }
149
+ return contact.publish({eventName, region, owner, payload});
149
150
  }
150
151
 
151
152
  // We represent handles and avatars by inserting stuff into given elements.
@@ -156,8 +157,9 @@ export class Agent {
156
157
  element.textContent = value || '(none)';
157
158
  element.value = value || ''; // Hack: handle input[type="text"] as well. Must be property assignment, not attribute.
158
159
  }
160
+ static spinner = 'images/loading.gif';
159
161
  static avatar(element, value) { // Update avatar element with value.
160
- element.innerHTML = value === null ? '(none)' : (value.startsWith('data') ? this.makeImage(value) : Agent.makeIdenticon(value));
162
+ element.innerHTML = value === null ? '(none)' : (value === this.spinner || value.startsWith('data') ? this.makeImage(value) : Agent.makeIdenticon(value));
161
163
  }
162
164
  static downsampleResolution = 128; // max height or width
163
165
  static makeImage(url) {
@@ -237,11 +239,8 @@ export class Agent {
237
239
  this.updateValue(null, 'private', 'avatar');
238
240
  };
239
241
  fileChooser.onchange = async event => {
240
- consume(event);
241
- if (!fileChooser.files.length) return;
242
- const blob = await P2PWebNetwork.downsampledBlob({blob: fileChooser.files[0], maxDimension: Agent.downsampleResolution});
243
- this.updateValue(await P2PWebNetwork.blob2dataURL(blob), 'private', 'avatar');
244
- console.log('clearing avatar selection');
242
+ await this.fileEvent(fileChooser, event, 'private');
243
+ console.log('cleared avatar selection');
245
244
  };
246
245
  fileChooser.click();
247
246
  });
@@ -263,6 +262,16 @@ export class Agent {
263
262
  content.parentElement.classList.toggle('hidden', true);
264
263
  };
265
264
  }
265
+ async fileEvent(fileChooser, event, scope) { // Update value from fileChooser change event, return dataURL;
266
+ consume(event);
267
+ if (!fileChooser.files.length) return '';
268
+ const file = fileChooser.files[0];
269
+ const maxDimension = Agent.downsampleResolution;
270
+ const blob = await P2PWebNetwork.downsampledBlob({blob: file, maxDimension});
271
+ const dataURL = await P2PWebNetwork.blob2dataURL(blob);
272
+ await this.updateValue(dataURL, scope, 'avatar');
273
+ return dataURL;
274
+ }
266
275
  static current = null;
267
276
  static tag = null;
268
277
  static identity = null;
@@ -275,7 +284,7 @@ export class Agent {
275
284
  static async initialize() { // Initialize what the agent needs from the about screen
276
285
  let tag = localStorage.getItem('usertag');
277
286
  const persistAs = tag || uuidv4(); // Give it SOMETHING to persistAs.
278
- const myIdentity = await createAuthorIdentity({persistAs});
287
+ const myIdentity = await P2PWebNetwork.createAuthorIdentity({persistAs});
279
288
  if (tag !== persistAs) { // Fix up persistence by moving it to where it need to go.
280
289
  // WARNING: This relies on undocumented behavior of createAuthorIdentity().
281
290
  const realTag = myIdentity.authorId;
@@ -304,17 +313,15 @@ export class Agent {
304
313
  fileChooser.oncancel = event => {
305
314
  consume(event);
306
315
  console.log('cancel my avatar');
316
+ myAgent.updateValue(this.spinner, 'public', 'avatar', false);
307
317
  myAgent.updateValue(null, 'public', 'avatar');
308
318
  myAgent.persistPrivate(null, 'avatar'); // So that we'll have it next session.
309
319
  console.log('clearing avatar selection');
310
320
  };
311
321
  fileChooser.onchange = async event => {
312
- consume(event);
313
- if (!fileChooser.files.length) return;
314
- const blob = await P2PWebNetwork.downsampledBlob({blob: fileChooser.files[0], maxDimension: Agent.downsampleResolution});
315
- const url = await P2PWebNetwork.blob2dataURL(blob);
322
+ myAgent.updateValue(this.spinner, 'public', 'avatar', false);
323
+ const url = await myAgent.fileEvent(fileChooser, event, 'public');
316
324
  console.log('set my avatar', url.slice(0, 50));
317
- myAgent.updateValue(url, 'public', 'avatar');
318
325
  myAgent.persistPrivate(url, 'avatar'); // So that we'll have it next session.
319
326
  };
320
327
  fileChooser.click();
@@ -7,8 +7,8 @@ import { consume } from './display.js';
7
7
  import { Hashtags } from './hashtags.js';
8
8
  import { Agent } from './agent.js';
9
9
  import { Conversation, Reply } from './conversation.js';
10
- import { alertTopic } from './versions.js';
11
- import { getContainingCells, findCoverCellsByCenterAndPoint } from './s2.js';
10
+ import { alertTopic, topicRegion, topicCell } from './versions.js';
11
+ import { getContainingCells, getSubdivision, findCoverCellsByCenterAndPoint } from './s2.js';
12
12
  const { localStorage, getComputedStyle, URL, URLSearchParams, domtoimage } = globalThis;
13
13
 
14
14
 
@@ -17,7 +17,7 @@ export function getShareableURL(tag = null, tags = Hashtags.getSubscribe()) { //
17
17
  const zoom = map.getZoom();
18
18
  const { lat, lng } = map.getCenter();
19
19
 
20
- params.set('tags', tags.map(tag => encodeURIComponent(tag)).join(','));
20
+ if (tags.length) params.set('tags', tags.map(tag => encodeURIComponent(tag)).join(','));
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);
@@ -68,7 +68,7 @@ export function go({lat = null, lng = null, zoom = null, alert = null}) { // Go
68
68
  }
69
69
  openOnReceive = null;
70
70
  if (alert) {
71
- Alert.openPopup(alert) || (openOnReceive = alert);
71
+ Alert.openPopup(alert);
72
72
  }
73
73
  }
74
74
 
@@ -94,58 +94,85 @@ class AlertReply extends Reply {
94
94
  const {dataURL:file, name, msgIds} = await contact.assembleChunkedDataURL(attachmentTopic);
95
95
  Object.assign(payload, {file, name, attachmentTopic, msgIds});
96
96
  }
97
- const element = container.startFader('.alert-commented', issuedTime + ttl - Date.now());
98
- element.style.display = 'block';
99
- // Restart the pulse animation by setting animationName to something it isn't.
100
- element.style.animationName = element.style.animationName === 'pulse2' ? 'pulse' : 'pulse2';
101
97
  container.showNotification({agent, issuedTime, body: payload.message || payload.name || payload});
102
98
  return this;
103
99
  }
100
+ update() { } // TODO: are we really getting multiple reply events for the same data?
104
101
  }
105
102
 
106
- let subscriptions = []; // array of stringy keys <mumble>:<cellID>:<hashtag>
107
- let subscriptionsRegion;
108
- // We do not record exactly where you were looking across sessions, but we do record the containing level 9 cell.
109
- let lastLevel9Cell; // S2 level 9 cells average a radius of about 10km ~ 6.5 miles.
110
-
111
- let last = []; // Last published lat, lng, tag
112
- const maxPublish = 5;
113
- let publishing = false;
114
-
103
+ import { s2 } from 's2js';
115
104
  export class Alert extends Conversation { // A wrapper around L.marker
116
105
  // When we resubscribe to different cells covering the same place, we will get the same
117
106
  // sticky data. We don't want to change the marker. Fortunately, the publication to each
118
107
  // of the cells (at different scales) are all published with the same data.
119
- static updateSubscriptions(oldKeys = subscriptions, newKeys) { // Update current subscriptions to the new map bounds.
120
- // A value of [] passed for oldKeys is used to start things off fresh (i.e., without supressing subscription of any carry-overs).
121
- if (!networkPromise) { console.warn("No network through which to subscribe."); return; } // Does this ever happen? Why?
122
- let region;
123
- if (!newKeys) { // None specified. Compute them.
124
- const center = map.getCenter();
125
- const bounds = map.getBounds();
126
- const northEast = bounds.getNorthEast();
127
- const newCells = findCoverCellsByCenterAndPoint(center.lat, center.lng, northEast.lat, northEast.lng); // array of cell IDs (BigInts)
128
- region = P2PWebNetwork.regionCode(center.lat, center.lng);
129
- newKeys = newCells.flatMap(cell => Hashtags.getSubscribe().map(hash => alertTopic(cell, hash)));
130
- Agent.current?.trackPublicChanges(region);
131
- // Record a zoomed-out cell id in case next session does not have geolocation services.
132
- let level9Cell = getContainingCells(center.lat, center.lng)[9];
133
- if (level9Cell !== lastLevel9Cell) localStorage.setItem('level9Cell', lastLevel9Cell = level9Cell);
134
- }
135
-
136
- const subscribe = (key, region, handler) =>
137
- networkPromise.then(async contact => contact.subscribe({eventName: key, region, handler}));
138
-
108
+ static subscriptions = {}; // maps currently active eventNames (<mumble>:<cellID>:<hashtag>) to count of event received for it.
109
+ // We do not record exactly where you were looking across sessions, but we do record the containing level 9 cell.
110
+ static lastLevel9Cell = null; // S2 level 9 cells average a radius of about 10km ~ 6.5 miles.
111
+ static subscriptionFromMap() { // Generate {eventName => count} for current map bounds.
112
+ const center = map.getCenter();
113
+ const bounds = map.getBounds();
114
+ const northEast = bounds.getNorthEast();
115
+ const newCells = findCoverCellsByCenterAndPoint(center.lat, center.lng, northEast.lat, northEast.lng); // array of cell IDs (BigInts)
116
+ if (!newCells) return null;
117
+ const region = P2PWebNetwork.regionCode(center.lat, center.lng);
118
+ const newKeys = {};
119
+ newCells.forEach(cell => Hashtags.getSubscribe().forEach(hash => {
120
+ const eventName = alertTopic(cell, hash);
121
+ newKeys[eventName] = this.subscriptions[eventName] || 0;
122
+ }));
123
+ Agent.current?.trackPublicChanges(region);
124
+ // Record a zoomed-out cell id in case next session does not have geolocation services.
125
+ let level9Cell = getContainingCells(center.lat, center.lng)[9];
126
+ if (level9Cell !== this.lastLevel9Cell) localStorage.setItem('level9Cell', this.lastLevel9Cell = level9Cell);
127
+ return newKeys;
128
+ }
129
+ static async updateSubscriptions({
130
+ oldKeys = this.subscriptions,
131
+ newKeys = this.subscriptionFromMap(),
132
+ throttleMS = 20
133
+ } = {}) { // Update current subscriptions.
134
+ // A value of {} passed for oldKeys is used to start things off fresh (i.e., without supressing subscription of any carry-overs).
135
+ if (!newKeys) return; // e.g., wacky computation. Don't change anything.
136
+ const contact = await networkPromise;
137
+ if (!contact) { console.warn("No network through which to subscribe."); return; } // Does this ever happen? Why?
138
+ this.subscriptions = newKeys; // Before subscribing.
139
+ const subscribe = async (key, handlerIn) => {
140
+ const handler = handlerIn && (properties => {
141
+ // When there are enough alerts within a topic, it rolls over and just reports the most recent number.
142
+ // When get near that count, we update the subscriptions such that the overloading topic is replaced
143
+ // with the topics for each of the four subcells that that make up the one with too many alerts.
144
+ // This repeats until we reach cells that are not rolling over.
145
+ const maxAlertsInCell = 900;
146
+ let count = ++newKeys[key];
147
+ handlerIn(properties);
148
+ if (count >= maxAlertsInCell) {
149
+ let nextKeys = {};
150
+ for (const old in newKeys) {
151
+ if (old !== key) {
152
+ nextKeys[old] = newKeys[old];
153
+ } else {
154
+ const cellTag = topicCell(key);
155
+ const subdivisions = getSubdivision(cellTag);
156
+ const topics = subdivisions.map(cellTag => alertTopic(cellTag, properties.hashtag));
157
+ console.log('subdividing', key, 'into', topics);
158
+ topics.forEach(topic => nextKeys[topic] = 0);
159
+ }
160
+ }
161
+ this.updateSubscriptions({oldKeys: newKeys, newKeys: nextKeys, throttleMS});
162
+ }
163
+ });
164
+ const region = topicRegion(key);
165
+ await contact.subscribe({eventName: key, region, handler}).then(() => throttleMS && P2PWebNetwork.delay(throttleMS));
166
+ };
167
+ console.log('updating subscriptions', {newKeys, oldKeys});
139
168
  // For each entry in the new subscription set that was not previously subscribed, subscribe now.
140
- for (const key of newKeys) oldKeys.includes(key) || subscribe(key, region, data => Alert.ensure(data));
141
-
169
+ for (const key in newKeys) oldKeys.hasOwnProperty(key) || await subscribe(key, data => Alert.ensure(data));
142
170
  // For each existing subscription, if it does not appear in the new set then unsubscribe.
143
- for (const key of oldKeys) newKeys.includes(key) || subscribe(key, subscriptionsRegion, null);
144
- console.log('Subscribed', {newKeys, region, length: newKeys.length, oldKeys, subscriptionsRegion});
145
-
146
- subscriptions = newKeys;
147
- subscriptionsRegion = region;
171
+ for (const key in oldKeys) newKeys.hasOwnProperty(key) || await subscribe(key, null);
148
172
  }
173
+ static maxPublish = 5;
174
+ static publishing = false;
175
+ static lastPublished = []; // Last published lat, lng, tag
149
176
  // Publish an alert to all applicable eventNames, canceling as required. Promises tag (msgId).
150
177
  static async publish({lat, lng,
151
178
  originalPosting = undefined,
@@ -160,21 +187,21 @@ export class Alert extends Conversation { // A wrapper around L.marker
160
187
  // However, the 'unpublishing' (if any) is invoked first.
161
188
  // To do this, we must hash the eventName ourselves.
162
189
  //console.log('publish', {lat, lng, hashtag, payload, cancel, subject, issuedTime, rest});
163
- if (publishing) { console.log('skiping overlapping publish'); return null; } // do not stack them up.
190
+ if (this.publishing) { console.log('skiping overlapping publish'); return null; } // do not stack them up.
164
191
  try {
165
- publishing = true;
192
+ this.publishing = true;
166
193
 
167
194
  const contact = await networkPromise; // subtle: The rest of this all happens synchronously, with any null payloads definitely first.
168
195
  let oldCells = null, oldHash, oldSubject = null; // Recorded for logging, below.
169
196
  let lastFillIn;
170
197
  if (payload) {
171
198
  lastFillIn = {lat, lng, hashtag, issuedTime};
172
- last.push(lastFillIn); // Capture the added data.
173
- const periodStart = Date.now() - (maxPublish * 60e3); // maxPublish minutes ago.
174
- last = last.filter(past => past.issuedTime >= periodStart);
175
- if (cancel === undefined && last.length > maxPublish) { // Unless specified otherwise, cancel oldest over maxPublish.
176
- showMessage(Int`Too many posts. (5 allowed every 5 minutes.) Removing oldest from this period.`);
177
- cancel = last.shift();
199
+ this.lastPublished.push(lastFillIn); // Capture the added data.
200
+ const periodStart = Date.now() - (this.maxPublish * 60e3); // maxPublish minutes ago.
201
+ this.lastPublished = this.lastPublished.filter(past => past.issuedTime >= periodStart);
202
+ if (cancel === undefined && this.lastPublished.length > this.maxPublish) { // Unless specified otherwise, cancel oldest over maxPublish.
203
+ showMessage(Int`Too many posts. (5 allowed every 5 minutes.) Removing oldest from this period.`, 'instructions');
204
+ cancel = this.lastPublished.shift();
178
205
  }
179
206
  }
180
207
  if (cancel) {
@@ -208,13 +235,13 @@ export class Alert extends Conversation { // A wrapper around L.marker
208
235
  }
209
236
  }
210
237
  if (!payload) {
211
- const index = last.findIndex(past => past.subject === subject);
212
- if (index >= 0) last.splice(index, 1);
238
+ const index = this.lastPublished.findIndex(past => past.subject === subject);
239
+ if (index >= 0) this.lastPublished.splice(index, 1);
213
240
  }
214
241
  console.log('Published', {cells, n: cells.length, region, hashtag, subject, payload, oldCells, oldHash, oldSubject});
215
242
  return subject;
216
243
  } finally {
217
- publishing = false;
244
+ this.publishing = false;
218
245
  }
219
246
  }
220
247
 
@@ -225,7 +252,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
225
252
  }
226
253
  static openPopup(alertTag) { // Open the marker specified by subject.
227
254
  const wrapper = this.getItem(alertTag);
228
- wrapper?.openPopup();
255
+ wrapper?.openPopup() || (openOnReceive = alertTag);
229
256
  }
230
257
  async openPopup() { // Open this wrapper's popup, and resolve any waiting promise.
231
258
  const { resolveGo } = this; // A handy hook for scripting.
@@ -444,9 +471,26 @@ export class Alert extends Conversation { // A wrapper around L.marker
444
471
  get itemKind() { // Answer class of reply items.
445
472
  return AlertReply;
446
473
  }
447
- async ensure(data) { // Add or update reply for this marker.
474
+ async ensure(data) { // Add or update reply for this reply.
448
475
  data.subject = data.tag; //fixme
476
+ const remaining = data.issuedTime + ttl - Date.now();
477
+ if (remaining < 0) return null;
449
478
  const reply = await super.ensure(data);
479
+ if (reply) {
480
+ if (reply === this.items[0]) { // If first sorted reply, and there's a message, update the tooltip.
481
+ const message = reply.payload?.message || (!reply.payload.file && reply.payload);
482
+ const markerElement = message && this.marker.getElement();
483
+ if (markerElement) tooltip(markerElement, message);
484
+ }
485
+ if (reply === this.items[this.items.length - 1]) { // If last reply so far (even if first), show ring and update fader.
486
+ const ringElement = this.startFader('.alert-commented', remaining);
487
+ if (ringElement) {
488
+ ringElement.style.display = 'block';
489
+ // Restart the pulse animation by setting animationName to something it isn't.
490
+ ringElement.style.animationName = ringElement.style.animationName === 'pulse2' ? 'pulse' : 'pulse2';
491
+ }
492
+ }
493
+ }
450
494
  this.needsRedisplay = true;
451
495
  this.ensureContent();
452
496
  return reply;
@@ -562,7 +606,9 @@ export class Alert extends Conversation { // A wrapper around L.marker
562
606
  }
563
607
  startFader(selector, remaining) { // Set up or update fader on the specified marker element, returning that element.
564
608
  const { marker } = this;
565
- const element = marker.getElement().querySelector(selector);
609
+ const markerElement = marker.getElement();
610
+ if (!markerElement) return null; // removed (e.g., if expired).
611
+ const element = markerElement.querySelector(selector);
566
612
  const fraction = remaining / ttl; // Start at 1 and go to 0, but we may be some way along that.
567
613
  const endOpacity = 0.5; // Fully transparent is 0, but that's too hard to see. :-)
568
614
  const endGrayscale = 1; // Fully gray.
@@ -572,7 +618,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
572
618
  element.style.opacity = opacity;
573
619
  // I'd like to let css transitions do the work, but as we zoom, we make different subscriptions and thus start
574
620
  // the "same" marker over again. This initial setup clashes with zooming if done with a next-tick step opacity+filter value.
575
- const interval = 2e3; // Milliseconds / step
621
+ const interval = 10e3; // Milliseconds / step
576
622
  const opacityFade = (endOpacity - opacity) * interval / remaining; // change / step
577
623
  const grayscaleFade = (endGrayscale - grayscale) * interval / remaining;
578
624
  clearInterval(this[selector]);
@@ -593,3 +639,4 @@ export class Alert extends Conversation { // A wrapper around L.marker
593
639
  super.destroy();
594
640
  }
595
641
  }
642
+ globalThis.Alert = Alert; // for debugging