@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.
- package/index.js +1 -0
- package/package.json +4 -2
- package/public/about/add-topic-autocomplete.png +0 -0
- package/public/about/add-topic.png +0 -0
- package/public/about/can-happen.jpg +0 -0
- package/public/about/en.html +72 -30
- package/public/about/identity.html +48 -0
- package/public/about/index.html +1 -1
- package/public/about/join.webp +0 -0
- package/public/about/notification.html +71 -0
- package/public/about/posters.jpeg +0 -0
- package/public/about/pwa.html +61 -0
- package/public/about/style.css +15 -2
- package/public/about/template.html +28 -0
- package/public/about/topics.png +0 -0
- package/public/about/train-radio-button.png +0 -0
- package/public/about/train.png +0 -0
- package/public/about/ukraine-fires.jpeg +0 -0
- package/public/about/ukraine-fires.png +0 -0
- package/public/index.html +17 -9
- package/public/javascripts/alert.js +203 -119
- package/public/javascripts/browser-push.js +56 -0
- package/public/javascripts/conversation.js +6 -3
- package/public/javascripts/display.js +2 -1
- package/public/javascripts/hashtags.js +3 -1
- package/public/javascripts/main.js +102 -64
- package/public/javascripts/map.js +13 -6
- package/public/javascripts/p2pWebNetwork.js +37 -4
- package/public/javascripts/protocol.js +3 -3
- package/public/javascripts/pubsub.js +107 -98
- package/public/javascripts/push.js +28 -0
- package/public/javascripts/s2.js +24 -2
- package/public/javascripts/service-manager.js +11 -7
- package/public/javascripts/store-memory.js +74 -0
- package/public/javascripts/store-redis.js +101 -0
- package/public/javascripts/translations.js +6 -6
- package/public/service-worker.js +50 -5
- package/public/stylesheets/style.css +50 -17
- package/server/websocket.js +12 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { pica, FileReader, File, URL } = globalThis;
|
|
2
|
-
import { resetInactivityTimer } from './main.js';
|
|
2
|
+
import { closeAll, resetInactivityTimer } from './main.js';
|
|
3
3
|
import { showMessage } from './map.js';
|
|
4
4
|
|
|
5
5
|
export function consume(event) { // i.e., don't close dialogs
|
|
@@ -11,6 +11,7 @@ export function openDisplay(containerIdentifier, event = null, content = undefin
|
|
|
11
11
|
// Open containerIdentifier with close handler attached and optional content, and return content element
|
|
12
12
|
const containerElement = document.getElementById(containerIdentifier);
|
|
13
13
|
consume(event);
|
|
14
|
+
closeAll();
|
|
14
15
|
|
|
15
16
|
containerElement.onclick = event => {
|
|
16
17
|
resetInactivityTimer();
|
|
@@ -23,6 +23,7 @@ let allKnownHashtags = JSON.parse(localStorage.getItem('allKnownHashtags') || `[
|
|
|
23
23
|
"🎙️ news",
|
|
24
24
|
"👁️ observer corps",
|
|
25
25
|
"🎵 pop",
|
|
26
|
+
"🚂 train",
|
|
26
27
|
"🧰 utility repairs"
|
|
27
28
|
]`);
|
|
28
29
|
|
|
@@ -125,7 +126,8 @@ export const Hashtags = {
|
|
|
125
126
|
if (resetSubscriptions) {
|
|
126
127
|
// We destroy unsubscribed markers right away, because we don't want the user to have to wait and wonder why they're still displayed.
|
|
127
128
|
// If there are alerts in flight, they will be rejected by Alert initialize because we will have already turned off the sub.
|
|
128
|
-
|
|
129
|
+
// The remaining have needsRedisplay set so that their menus are correct.
|
|
130
|
+
Alert.items.forEach(wrapper => (this.isSubscribed(wrapper.hashtag) && (wrapper.needsRedisplay = true)) || wrapper.destroy());
|
|
129
131
|
Alert.updateSubscriptions();
|
|
130
132
|
}
|
|
131
133
|
},
|
|
@@ -4,10 +4,13 @@ import { Int } from './translations.js';
|
|
|
4
4
|
import { openDisplay } from './display.js';
|
|
5
5
|
import { Agent} from './agent.js';
|
|
6
6
|
import { P2PWebNetwork } from './p2pWebNetwork.js';
|
|
7
|
-
import { getPointInCell } from './s2.js';
|
|
7
|
+
import { getPointInCell, getSmallestCellSurrounding } from './s2.js';
|
|
8
8
|
import { Alert, getShareableURL, share } from './alert.js';
|
|
9
|
+
import { Hashtags } from './hashtags.js';
|
|
9
10
|
import { map, showMessage, updateLocation, recenterMap } from './map.js';
|
|
10
|
-
import './service-manager.js'; // Comment this out and kill service-workers for reload-to-get-latest behavior during development.
|
|
11
|
+
import {postServiceMessage} from './service-manager.js'; // Comment this out and kill service-workers for reload-to-get-latest behavior during development.
|
|
12
|
+
//const postServiceMessage = null;
|
|
13
|
+
export {postServiceMessage};
|
|
11
14
|
window.P2PWebNetwork = P2PWebNetwork;
|
|
12
15
|
|
|
13
16
|
document.getElementById('appVersion').textContent = appVersion;
|
|
@@ -24,8 +27,6 @@ var inactivityTimer = null, reconnectCountdown, networkPromise = null;
|
|
|
24
27
|
export { networkPromise };
|
|
25
28
|
export async function resetInactivityTimer(clearMessage = true) { // if !network, initialize(false), else disconnect after INACTIVITY_SECONDSif not restarted
|
|
26
29
|
//console.log('resetInactivityTimer, networkPromise:', networkPromise);
|
|
27
|
-
document.body.classList.toggle('firstConversation', false);
|
|
28
|
-
document.body.classList.toggle('firstPublish', false);
|
|
29
30
|
if (clearMessage) showMessage('');
|
|
30
31
|
clearTimeout(inactivityTimer);
|
|
31
32
|
clearInterval(reconnectCountdown);
|
|
@@ -58,7 +59,7 @@ function isWebView() { return /CriOS|(WebView|wv|(iPhone|iPod|iPad)(?!.*Safari))
|
|
|
58
59
|
function isApple() { return navigator.platform.startsWith('Mac') || ['iPhone', 'iPad'].includes(navigator.platform); }
|
|
59
60
|
function isMobile() { return navigator.userAgentData?.mobile || /iPhone|iPad|iPod|Mobile/.test(navigator.userAgent); }
|
|
60
61
|
function isStandalone() { return window.matchMedia('(display-mode: standalone)').matches; }
|
|
61
|
-
export function osName() { return navigator.userAgentData?.platform || navigator.userAgent.match(/Android/)?.[0] || navigator.platform; }
|
|
62
|
+
export function osName() { return isApple() ? (isMobile() ? 'iOS' : 'OSX') : navigator.userAgentData?.platform || navigator.userAgent.match(/Android/)?.[0] || navigator.platform; }
|
|
62
63
|
function mobilePlatformName() { return isMobile() && (isApple() ? 'iOS' : 'Android'); }
|
|
63
64
|
function mobileVendorName() { return isMobile() && (isApple() ? 'Apple' : 'Android'); }
|
|
64
65
|
function mobileBrowserName() { return isApple() ? 'Safari' : 'Chrome'; }
|
|
@@ -73,44 +74,54 @@ function browserName() {
|
|
|
73
74
|
|
|
74
75
|
var showNotifications = document.getElementById('showNotifications');
|
|
75
76
|
var showNotificationsLabel = document.getElementById('showNotificationsLabel');
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
function
|
|
77
|
+
function notificationsRequested() { // Explicit choice by user in app, but might be currently denied by OS.
|
|
78
|
+
return !!localStorage.getItem('notificationsRequested');
|
|
79
|
+
}
|
|
80
|
+
export function notificationsAllowed() { // Combined result.
|
|
81
|
+
return postServiceMessage && (Notification?.permission === 'granted') && notificationsRequested();
|
|
82
|
+
}
|
|
83
|
+
let notificationsPreviouslyAllowed = notificationsAllowed();
|
|
84
|
+
function noteNotificationPermission(permission) { // Update permission controls/text/persistence.
|
|
85
|
+
// Called when checkbox is toggled, opening dialog, or if platform happens to tell us permission was externally changed (e.g., even if dialog already open).
|
|
80
86
|
if (isWebView()) {
|
|
87
|
+
showNotifications.checked = false;
|
|
81
88
|
showNotifications.indeterminate = true;
|
|
82
89
|
showNotifications.toggleAttribute('disabled', true);
|
|
83
90
|
showNotificationsLabel.innerHTML = `${mobileVendorName()} ${Int`does not support notifications on WebViews embedded in other programs. Please use CivilDefense.io in native`} ${mobileBrowserName()}.`;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (isMobile() && isApple() && !isStandalone()) {
|
|
91
|
+
} else if (isMobile() && isApple() && !isStandalone()) {
|
|
92
|
+
showNotifications.checked = false;
|
|
87
93
|
showNotifications.indeterminate = true;
|
|
88
94
|
showNotifications.toggleAttribute('disabled', true);
|
|
89
95
|
showNotificationsLabel.innerHTML = `${Int`Apple only supports mobile notifications for web pages that have been`} <a href="https://www.google.com/search?q=iphone+install+web+page+to+home+screen" target="yz.sidebar">${Int`installed to the home screen`}</a>.`;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
96
|
+
} else {
|
|
97
|
+
switch (permission) {
|
|
98
|
+
case 'default':
|
|
99
|
+
showNotifications.indeterminate = true;
|
|
100
|
+
showNotifications.toggleAttribute('disabled', false);
|
|
101
|
+
showNotificationsLabel.innerHTML = Int`Enable notifications`;
|
|
102
|
+
break;
|
|
103
|
+
case 'granted':
|
|
104
|
+
showNotifications.checked = notificationsRequested();
|
|
105
|
+
showNotifications.indeterminate = false;
|
|
106
|
+
showNotifications.toggleAttribute('disabled', false);
|
|
107
|
+
showNotificationsLabel.innerHTML = Int`Allow notifications`;
|
|
108
|
+
break;
|
|
109
|
+
default: // Permission is explicitly not granted.
|
|
110
|
+
showNotifications.checked = false;
|
|
111
|
+
showNotifications.indeterminate = false;
|
|
112
|
+
showNotifications.toggleAttribute('disabled', true);
|
|
113
|
+
const isApp = isStandalone();
|
|
114
|
+
const search = isApp ?
|
|
115
|
+
`https://www.google.com/search?q=${osName()}+open+app+settings` :
|
|
116
|
+
`https://www.google.com/search?q=open+site+settings+${browserName() || `"${navigator.userAgent}"`}`;
|
|
117
|
+
const label = isApp ? `CivilDefense.io ${Int`app`}` : Int`browser site settings`;
|
|
118
|
+
showNotificationsLabel.innerHTML = `${Int`Permissions can be re-enabled through the`} <a href="${search}" target="yz.sidebar">${label}</a>.`;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
113
121
|
}
|
|
122
|
+
if (showNotifications.checked === notificationsPreviouslyAllowed) return;
|
|
123
|
+
notificationsPreviouslyAllowed = showNotifications.checked;
|
|
124
|
+
Alert.updateSubscriptions('stickyChange');
|
|
114
125
|
}
|
|
115
126
|
clickTip(showNotifications.parentElement, Int`Enable local ${osName()} notifcations for map alerts, without going through any servers. Requires that the app be running.`, event => {
|
|
116
127
|
resetInactivityTimer();
|
|
@@ -118,15 +129,23 @@ clickTip(showNotifications.parentElement, Int`Enable local ${osName()} notifcati
|
|
|
118
129
|
});
|
|
119
130
|
showNotifications.onchange = () => {
|
|
120
131
|
if (window.Notification?.permission === 'granted') {
|
|
121
|
-
|
|
132
|
+
localStorage.setItem('notificationsRequested', showNotifications.checked ? '1' : '');
|
|
133
|
+
noteNotificationPermission('granted');
|
|
122
134
|
} else {
|
|
123
|
-
window.Notification?.requestPermission().then(noteNotificationPermission);
|
|
135
|
+
window.Notification?.requestPermission().then(noteNotificationPermission());
|
|
124
136
|
}
|
|
125
137
|
};
|
|
126
138
|
// Safari never fires 'change': https://webkit.org/b/259432
|
|
127
139
|
navigator.permissions.query({ name: 'notifications'})
|
|
128
140
|
.then(status => status.onchange = () => noteNotificationPermission(window.Notification?.permission));
|
|
129
141
|
|
|
142
|
+
export function closeAll() { // Close anything that might be open.
|
|
143
|
+
closeAbout();
|
|
144
|
+
Alert.closePopup();
|
|
145
|
+
Hashtags.closeSelector();
|
|
146
|
+
['aboutContainer', 'updateContainer', 'correspondentContainer', 'qrContainer']
|
|
147
|
+
.forEach(tag => document.getElementById(tag).classList.toggle('hidden', true));
|
|
148
|
+
}
|
|
130
149
|
export function openAbout(event) {
|
|
131
150
|
openDisplay('aboutContainer', event);
|
|
132
151
|
noteNotificationPermission(window.Notification?.permission);
|
|
@@ -134,17 +153,6 @@ export function openAbout(event) {
|
|
|
134
153
|
export function closeAbout() {
|
|
135
154
|
document.getElementById('aboutContainer').classList.toggle('hidden', true);
|
|
136
155
|
}
|
|
137
|
-
clickTip('#aboutButton', Int`Information about this app, and options to change notifications or how you appear to others.`, event => { // open about
|
|
138
|
-
Alert.closePopup();
|
|
139
|
-
openAbout(event);
|
|
140
|
-
});
|
|
141
|
-
clickTip('#wipe', Int`Wipe from ${osName()} all personal data and source files for this app.`, async event => {
|
|
142
|
-
await networkPromise?.then(contact => contact.disconnect());
|
|
143
|
-
localStorage.clear();
|
|
144
|
-
await caches.keys().then(keys => Promise.all(keys.map(key => caches.delete(key))));
|
|
145
|
-
await navigator.serviceWorker.getRegistrations().then(registrations => Promise.all(registrations.map(r => r.unregister())));
|
|
146
|
-
window.location.replace('about/index.html');
|
|
147
|
-
});
|
|
148
156
|
document.getElementById('scriptChooser').onchange = event => { // Run a script module chosen by the user. e.g., for testing.
|
|
149
157
|
const file = event.currentTarget.files[0];
|
|
150
158
|
if (!file) return;
|
|
@@ -184,10 +192,11 @@ document.querySelector('#correspondentContainer md-outlined-text-field').onclick
|
|
|
184
192
|
event.stopPropagation();
|
|
185
193
|
};
|
|
186
194
|
|
|
187
|
-
|
|
188
|
-
event
|
|
195
|
+
export function shareMap(event) {
|
|
196
|
+
event?.stopPropagation();
|
|
189
197
|
share({text: "CivilDefense.io", url: getShareableURL().href });
|
|
190
|
-
}
|
|
198
|
+
}
|
|
199
|
+
clickTip('#share', Int`${osName()} share a link to this map and topics, with picture of map.`, shareMap);
|
|
191
200
|
|
|
192
201
|
clickTip('#recenterButton', Int`Recenter the map to where you are in the world.`, recenterMap);
|
|
193
202
|
|
|
@@ -218,6 +227,7 @@ export let positionWatch;
|
|
|
218
227
|
let subscribeOneShot;
|
|
219
228
|
const goodPositionLabel = Int`Your Location`;
|
|
220
229
|
|
|
230
|
+
export let lastCells = null;
|
|
221
231
|
function initializeGeolocation(subscribe = false) { // Arrange to constantly updateLocation, but:
|
|
222
232
|
// Arrange to constantly updateLocation when geolocation updates (or a default init if that's not possible).
|
|
223
233
|
// Once updateLocation has been called, updateSubscriptions() if the subscribe arg is true.
|
|
@@ -248,13 +258,15 @@ function initializeGeolocation(subscribe = false) { // Arrange to constantly upd
|
|
|
248
258
|
zoom = 13;
|
|
249
259
|
[lat, lng] = [37.7749, -122.4194]; // San Fransisco
|
|
250
260
|
}
|
|
261
|
+
} else {
|
|
262
|
+
lastCells = getSmallestCellSurrounding(lat, lng);
|
|
251
263
|
}
|
|
252
264
|
//console.log('initializeGeolocation updateLocation');
|
|
253
265
|
updateLocation(lat, lng, zoom, positionLabel);
|
|
254
266
|
if (!subscribeOneShot) return;
|
|
255
267
|
subscribeOneShot = false;
|
|
256
268
|
resetInactivityTimer(false);
|
|
257
|
-
Alert.updateSubscriptions(
|
|
269
|
+
Alert.updateSubscriptions('newNode');
|
|
258
270
|
};
|
|
259
271
|
if (!geolocation) {
|
|
260
272
|
showMessage(Int`Geolocation not supported. Using default location.`, 'error', 'fail');
|
|
@@ -276,7 +288,7 @@ function initializeGeolocation(subscribe = false) { // Arrange to constantly upd
|
|
|
276
288
|
delay(2e3).then(() => initMap());
|
|
277
289
|
} else {
|
|
278
290
|
showMessage(Int`Unable to get location.`, 'error', error);
|
|
279
|
-
delay(
|
|
291
|
+
delay(2e3).then(() => initMap());
|
|
280
292
|
}
|
|
281
293
|
} else {
|
|
282
294
|
showMessage(Int`No network connection.`, 'error');
|
|
@@ -320,7 +332,8 @@ async function initialize(event) { // Ensure there is a network promise and map,
|
|
|
320
332
|
const {promise, resolve} = Promise.withResolvers();
|
|
321
333
|
networkPromise = promise;
|
|
322
334
|
console.log('Creating node.');
|
|
323
|
-
|
|
335
|
+
Alert.clearPushData();
|
|
336
|
+
resolve(await P2PWebNetwork.create({pushPersistor: postServiceMessage && localStorage}));
|
|
324
337
|
networkPromise.then(contact => {
|
|
325
338
|
globalThis.contact = contact; // For debugging.
|
|
326
339
|
// On leaving, we would like to copy stored data and politely say 'bye' (so others can clean up their connections). Alas:
|
|
@@ -366,11 +379,21 @@ async function initialize(event) { // Ensure there is a network promise and map,
|
|
|
366
379
|
}
|
|
367
380
|
document.addEventListener('visibilitychange', initialize);
|
|
368
381
|
window.addEventListener('online', initialize);
|
|
382
|
+
window.addEventListener("appinstalled", async () => {
|
|
383
|
+
// It turns out that the same browser that support this push the EXISTING browsing context into their own frame,
|
|
384
|
+
// rather than starting fresh. (Which makes some sense for having this event fire.)
|
|
385
|
+
// In such cases, we need to re-initialize stuff. We could go point by point, but for now, just hit everything.
|
|
386
|
+
location.reload();
|
|
387
|
+
});
|
|
388
|
+
document.body.addEventListener('animationend', event => {
|
|
389
|
+
event.target.style.animation = '';
|
|
390
|
+
});
|
|
369
391
|
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
392
|
+
const titleLabel = document.querySelector('#titleLabel');
|
|
393
|
+
titleLabel.textContent = Int([titleLabel.textContent]);
|
|
394
|
+
const titleHostname = document.querySelector('#titleHostname');
|
|
395
|
+
titleHostname.textContent = ` @${location.hostname}`;
|
|
396
|
+
document.querySelector('head > title').innerHTML = titleLabel.textContent + titleHostname.textContent;
|
|
374
397
|
// Set up text for the browser language.
|
|
375
398
|
function initText(selector, content = selector) {
|
|
376
399
|
const element = document.querySelector(selector);
|
|
@@ -380,10 +403,6 @@ function initText(selector, content = selector) {
|
|
|
380
403
|
initText('#aboutReport');
|
|
381
404
|
initText('#aboutShared');
|
|
382
405
|
initText('#aboutFade');
|
|
383
|
-
initText('#aboutAnyone1');
|
|
384
|
-
initText('#aboutAnyone2');
|
|
385
|
-
initText('#aboutAnyone3');
|
|
386
|
-
initText('#learnMore');
|
|
387
406
|
initText('#version');
|
|
388
407
|
initText('#checkForUpdates');
|
|
389
408
|
initText('#downloadUpdates');
|
|
@@ -398,14 +417,33 @@ initText('#describePrivate2');
|
|
|
398
417
|
initText('#describePublic');
|
|
399
418
|
initText('#describeSystem');
|
|
400
419
|
initText('#pickLabels');
|
|
401
|
-
initText('#wipe');
|
|
402
420
|
initText('.firstConversation .teach.correspondent');
|
|
403
421
|
initText('.firstPublish .teach.correspondent');
|
|
404
422
|
initText('.teach.share');
|
|
405
423
|
initText('.teach.attach');
|
|
406
424
|
initText('.teach.changeHashtag');
|
|
407
425
|
|
|
426
|
+
clickTip('#aboutButton', Int`Information about this app, and options to change notifications or how you appear to others.`, event => { // open about
|
|
427
|
+
closeAll();
|
|
428
|
+
openAbout(event);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
if (isStandalone()) {
|
|
432
|
+
document.querySelector('#wipe').textContent = Int`Installed`;
|
|
433
|
+
} else {
|
|
434
|
+
initText('#wipe');
|
|
435
|
+
clickTip('#wipe', Int`Wipe from ${osName()} all personal data and source files for this app.`, async event => {
|
|
436
|
+
await networkPromise?.then(contact => contact.disconnect());
|
|
437
|
+
localStorage.clear();
|
|
438
|
+
await caches.keys().then(keys => Promise.all(keys.map(key => caches.delete(key))));
|
|
439
|
+
await navigator.serviceWorker.getRegistrations().then(registrations => Promise.all(registrations.map(r => r.unregister())));
|
|
440
|
+
window.location.replace('about/index.html');
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
408
444
|
tooltip('#learnMore', Int`Click to see more information in another tab about what you can do with this app and how it is resistant to tracking, censorship, and takedown.`);
|
|
409
|
-
tooltip('#
|
|
445
|
+
tooltip('#learnNotification', Int`Click to learn about receiving operating system notifications.`);
|
|
446
|
+
tooltip('#learnIdentity', Int`Click to learn about how you can choose to label yourself and how to recognize other users.`);
|
|
447
|
+
tooltip('#learnPWA', Int`Click to learn about installation, updates, and removal.`);
|
|
410
448
|
|
|
411
449
|
initialize(false);
|
|
@@ -3,6 +3,7 @@ import * as L from 'leaflet';
|
|
|
3
3
|
import { Int } from './translations.js';
|
|
4
4
|
import { consume } from './display.js';
|
|
5
5
|
import { Agent } from './agent.js';
|
|
6
|
+
import { networkPromise, closeAll } from './main.js';
|
|
6
7
|
import { P2PWebNetwork } from './p2pWebNetwork.js';
|
|
7
8
|
import { Alert, go } from './alert.js';
|
|
8
9
|
import { resetInactivityTimer, tooltip } from './main.js';
|
|
@@ -31,9 +32,13 @@ export function showMessage(message, type = 'loading', errorObject) { // Show lo
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
function flashElement(selector) {
|
|
36
|
+
const element = document.querySelector(selector);
|
|
37
|
+
setTimeout(() => element.style.animation = 'pulse20border ease 2s 1', 100);
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
let yourLocation; // marker
|
|
35
41
|
let lastLatitude, lastLongitude;
|
|
36
|
-
|
|
37
42
|
export function updateLocation(lat, lng, zoom, positionLabel) { // initMap if necessary, and set our position.
|
|
38
43
|
//console.log('updateLocation', lat, lng, map, yourLocation);
|
|
39
44
|
// Can't call getCurrentPosition while watching. So set it here for use in recenterMap.
|
|
@@ -47,15 +52,16 @@ export function updateLocation(lat, lng, zoom, positionLabel) { // initMap if ne
|
|
|
47
52
|
const params = url.searchParams;
|
|
48
53
|
const tags = params.get('tags');
|
|
49
54
|
const tagsArray = tags?.split(',') || [];
|
|
55
|
+
const highlight = params.get('highlight');
|
|
50
56
|
tagsArray.forEach(tag => Hashtags.add(decodeURIComponent(tag)));
|
|
51
57
|
Hashtags.onchange({resetSubscriptions: false}); // Too early to subscribe, but will be done during initialization.
|
|
52
58
|
go({lat: params.get('lat'), lng: params.get('lng'), zoom: params.get('z'), alert: params.get('alert')});
|
|
53
59
|
// We don't need the query parameters now. Get rid of them. They're annoying.
|
|
54
60
|
if (params.size > 0) {
|
|
55
|
-
['tags', 'lat', 'lng', 'z', 'alert'].forEach(key => params.delete(key));
|
|
61
|
+
['tags', 'lat', 'lng', 'z', 'alert', 'highlight'].forEach(key => params.delete(key));
|
|
56
62
|
history.replaceState(null, '', url);
|
|
57
63
|
}
|
|
58
|
-
|
|
64
|
+
if (highlight) flashElement(highlight);
|
|
59
65
|
return;
|
|
60
66
|
}
|
|
61
67
|
// Otherwise just update the yourLocation marker if appropriate (and not update zoom).
|
|
@@ -73,7 +79,7 @@ export function updateLocation(lat, lng, zoom, positionLabel) { // initMap if ne
|
|
|
73
79
|
|
|
74
80
|
export function recenterMap(event) {
|
|
75
81
|
consume(event);
|
|
76
|
-
|
|
82
|
+
closeAll();
|
|
77
83
|
const latLng = [lastLatitude, lastLongitude];
|
|
78
84
|
map.flyTo(latLng);
|
|
79
85
|
}
|
|
@@ -142,9 +148,10 @@ export function initMap(lat, lng, zoom, positionLabel) { // Set up appropriate z
|
|
|
142
148
|
resetInactivityTimer();
|
|
143
149
|
map.closePopup(yourLocation.getPopup());
|
|
144
150
|
});
|
|
145
|
-
map.on('moveend', () => {
|
|
146
|
-
|
|
151
|
+
map.on('moveend', async () => {
|
|
152
|
+
await networkPromise;
|
|
147
153
|
updateLocation(lastLatitude, lastLongitude); // Might now be within map.
|
|
154
|
+
Alert.updateSubscriptions();
|
|
148
155
|
});
|
|
149
156
|
|
|
150
157
|
// Add click event to note position
|
|
@@ -25,6 +25,8 @@ export class P2PWebNetwork {
|
|
|
25
25
|
static setSessionLocation = resolveSessionLocation;
|
|
26
26
|
static sessionLocation = sessionLocationPromise;
|
|
27
27
|
static async create({infoLogger = console.log, debugLogger,
|
|
28
|
+
// Pass a Storage object to clear peristed push subscriptions, and persist current ones for next session.
|
|
29
|
+
pushPersistor = null,
|
|
28
30
|
location = this.sessionLocation,
|
|
29
31
|
bridgeUrl = (globalThis.location && new URL(globalThis.location).searchParams.get('bridge')) ||
|
|
30
32
|
globalThis.process?.env.BRIDGE_URL ||
|
|
@@ -47,7 +49,7 @@ export class P2PWebNetwork {
|
|
|
47
49
|
});
|
|
48
50
|
infoLogger ||= null;
|
|
49
51
|
debugLogger ||= null;
|
|
50
|
-
Object.assign(network, {infoLogger, debugLogger, disconnector: disconnect, transport, nodeIdentity, peer});
|
|
52
|
+
Object.assign(network, {infoLogger, debugLogger, disconnector: disconnect, transport, nodeIdentity, peer, pushPersistor});
|
|
51
53
|
|
|
52
54
|
network.info(`Created network node for kernel ${this.kernelVersion} region 0x${this.regionCode(location.lat, location.lng).toString(16)}.`);
|
|
53
55
|
peer.onError(error => {
|
|
@@ -60,9 +62,30 @@ export class P2PWebNetwork {
|
|
|
60
62
|
peer.onLog('error', (...rest) => network.info('ERROR', ...rest));
|
|
61
63
|
const { peers, ms } = status;
|
|
62
64
|
network.info(`Connected ${peers} connections through ${bridgeUrl} in ${ms.toLocaleString()} ms.`);
|
|
65
|
+
await network.resetPersisted();
|
|
63
66
|
network.attached(network);
|
|
64
67
|
return network;
|
|
65
68
|
}
|
|
69
|
+
static pushPersistKey = 'lastPushed';
|
|
70
|
+
currentTopics = new Set();
|
|
71
|
+
pushPersist() { // Add/remove topics
|
|
72
|
+
if (!this.pushPersistor) return;
|
|
73
|
+
this.pushPersistor.setItem(this.constructor.pushPersistKey, JSON.stringify([this.nodeIdentity.id, ...this.currentTopics]));
|
|
74
|
+
}
|
|
75
|
+
async resetPersisted() { // Clears any sticky subscriptions in network and for browser's pushManager.
|
|
76
|
+
const {pushPersistor, peer} = this;
|
|
77
|
+
const lastPushed = pushPersistor && pushPersistor.getItem(this.constructor.pushPersistKey);
|
|
78
|
+
if (lastPushed) {
|
|
79
|
+
const [pushId, ...topics] = JSON.parse(lastPushed);
|
|
80
|
+
await Promise.all([
|
|
81
|
+
navigator.serviceWorker.ready
|
|
82
|
+
.then(registration => registration.pushManager.getSubscription())
|
|
83
|
+
.then(subscription => subscription?.unsubscribe()),
|
|
84
|
+
...topics.map(topic => peer.unsub(topic, {pushId}))
|
|
85
|
+
]);
|
|
86
|
+
this.pushPersist();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
66
89
|
|
|
67
90
|
async disconnect(debugLogger = this.debugLogger) { // Politely close network connection.
|
|
68
91
|
const health = this.peer.health();
|
|
@@ -179,7 +202,11 @@ export class P2PWebNetwork {
|
|
|
179
202
|
// The methods publish/subscribe map from the original civildefense-over-kdht API to Axona, and could be rewritten in the apps.
|
|
180
203
|
// But since we needed this class anyway, it was easiest to retain them.
|
|
181
204
|
// Besides, I don't like to see abbreviations in API names.
|
|
182
|
-
async subscribe({eventName, region, owner, since = 'all',
|
|
205
|
+
async subscribe({eventName, region, owner, since = 'all',
|
|
206
|
+
handler, // If truthy, associates the handler with topic. If falsy, removes all such handlers from topic.
|
|
207
|
+
pushData, // If handler and truthy (a browser subscription object extended with keys), create a sticky subscription with that.
|
|
208
|
+
pushId, // If no handler and truthy (a previous session nodeId), then cancel that session's sticky subscription.
|
|
209
|
+
pushPersist = this.pushPersistor}) { // If non-null, call this.pushPersist to save sticky data for undoing next session. (See lastPushed in create().)
|
|
183
210
|
await this.attachment;
|
|
184
211
|
const topic = {region, name: eventName};
|
|
185
212
|
if (owner) topic.owner = owner;
|
|
@@ -194,9 +221,15 @@ export class P2PWebNetwork {
|
|
|
194
221
|
}
|
|
195
222
|
handler({...message, agent: signerPubkey, tag: msgId, topic, ts});
|
|
196
223
|
};
|
|
197
|
-
await this.peer.sub(topic, callback, {since});
|
|
224
|
+
const result = await this.peer.sub(topic, callback, {since, pushData});
|
|
225
|
+
this.currentTopics.add(topic); // topic rather than result.topicId. See https://github.com/axona-net/axona-protocol/issues/64
|
|
226
|
+
if (pushPersist) this?.pushPersist();
|
|
227
|
+
return result;
|
|
198
228
|
} else {
|
|
199
|
-
|
|
229
|
+
const result = this.peer.unsub(topic, {pushId}); // See ticket ref'd above, altough topicId will have to be derived in this case.
|
|
230
|
+
this.currentTopics.delete(topic);
|
|
231
|
+
if (pushPersist) this?.pushPersist();
|
|
232
|
+
return result;
|
|
200
233
|
}
|
|
201
234
|
}
|
|
202
235
|
static currentPublishIdentity = null;
|
|
@@ -139,9 +139,9 @@ if (dht < 1) {
|
|
|
139
139
|
disconnect();
|
|
140
140
|
},
|
|
141
141
|
async sub(topic, handler, options) {
|
|
142
|
-
const
|
|
143
|
-
handlers[
|
|
144
|
-
return
|
|
142
|
+
const { topicName, topicId, id, pushPubkey } = await send('subscribe', topic, nodeTag, options);
|
|
143
|
+
handlers[id] = handler;
|
|
144
|
+
return {topicName, topicId, id, pushPubkey, addPushData: data => send('track', topicName, nodeTag, data)};
|
|
145
145
|
},
|
|
146
146
|
async unsub(topic, options) {
|
|
147
147
|
const result = await send('unsubscribe', topic, nodeTag, options);
|