@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 +2 -1
- package/package.json +5 -3
- package/public/images/loading.gif +0 -0
- package/public/index.html +5 -6
- package/public/javascripts/agent.js +37 -30
- package/public/javascripts/alert.js +106 -59
- package/public/javascripts/hashtags.js +121 -84
- package/public/javascripts/main.js +6 -2
- package/public/javascripts/p2pWebNetwork.js +31 -10
- package/public/javascripts/s2.js +7 -3
- package/public/javascripts/service-manager.js +5 -4
- package/public/javascripts/translations.js +1 -0
- package/public/javascripts/versions.js +16 -5
- package/public/service-worker.js +7 -7
- package/public/stylesheets/style.css +5 -4
- package/server/app.js +34 -23
- package/server/{location.json → location.save.json} +1 -1
- package/spec/axonSpec.js +9 -8
- package/public/junk.html +0 -145
- package/public/material-combobox.html +0 -379
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.
|
|
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.
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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,
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
173
|
-
const periodStart = Date.now() - (maxPublish * 60e3); // maxPublish minutes ago.
|
|
174
|
-
|
|
175
|
-
if (cancel === undefined &&
|
|
176
|
-
showMessage(Int`Too many posts. (5 allowed every 5 minutes.) Removing oldest from this period
|
|
177
|
-
cancel =
|
|
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 =
|
|
212
|
-
if (index >= 0)
|
|
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
|
|
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
|
|
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 =
|
|
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
|