@yz-social/civildefense.io 4.5.28 → 5.0.0
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/en.html +42 -9
- package/public/about/identity.html +33 -0
- package/public/about/index.html +1 -1
- package/public/about/notification.html +33 -0
- package/public/about/pwa.html +57 -0
- package/public/about/style.css +6 -0
- package/public/about/template.html +28 -0
- package/public/about/topics.png +0 -0
- package/public/index.html +17 -9
- package/public/javascripts/alert.js +140 -84
- 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 +2 -1
- package/public/javascripts/main.js +94 -60
- package/public/javascripts/map.js +5 -3
- 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 +10 -1
- package/server/getLocation.js +2 -2
- package/server/websocket.js +8 -4
|
@@ -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.refreshPushSubscriptions();
|
|
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;
|
|
@@ -218,6 +226,7 @@ export let positionWatch;
|
|
|
218
226
|
let subscribeOneShot;
|
|
219
227
|
const goodPositionLabel = Int`Your Location`;
|
|
220
228
|
|
|
229
|
+
export let lastCells = null;
|
|
221
230
|
function initializeGeolocation(subscribe = false) { // Arrange to constantly updateLocation, but:
|
|
222
231
|
// Arrange to constantly updateLocation when geolocation updates (or a default init if that's not possible).
|
|
223
232
|
// Once updateLocation has been called, updateSubscriptions() if the subscribe arg is true.
|
|
@@ -248,6 +257,8 @@ function initializeGeolocation(subscribe = false) { // Arrange to constantly upd
|
|
|
248
257
|
zoom = 13;
|
|
249
258
|
[lat, lng] = [37.7749, -122.4194]; // San Fransisco
|
|
250
259
|
}
|
|
260
|
+
} else {
|
|
261
|
+
lastCells = getSmallestCellSurrounding(lat, lng);
|
|
251
262
|
}
|
|
252
263
|
//console.log('initializeGeolocation updateLocation');
|
|
253
264
|
updateLocation(lat, lng, zoom, positionLabel);
|
|
@@ -276,7 +287,7 @@ function initializeGeolocation(subscribe = false) { // Arrange to constantly upd
|
|
|
276
287
|
delay(2e3).then(() => initMap());
|
|
277
288
|
} else {
|
|
278
289
|
showMessage(Int`Unable to get location.`, 'error', error);
|
|
279
|
-
delay(
|
|
290
|
+
delay(2e3).then(() => initMap());
|
|
280
291
|
}
|
|
281
292
|
} else {
|
|
282
293
|
showMessage(Int`No network connection.`, 'error');
|
|
@@ -320,7 +331,8 @@ async function initialize(event) { // Ensure there is a network promise and map,
|
|
|
320
331
|
const {promise, resolve} = Promise.withResolvers();
|
|
321
332
|
networkPromise = promise;
|
|
322
333
|
console.log('Creating node.');
|
|
323
|
-
|
|
334
|
+
Alert.clearPushData();
|
|
335
|
+
resolve(await P2PWebNetwork.create({pushPersistor: postServiceMessage && localStorage}));
|
|
324
336
|
networkPromise.then(contact => {
|
|
325
337
|
globalThis.contact = contact; // For debugging.
|
|
326
338
|
// On leaving, we would like to copy stored data and politely say 'bye' (so others can clean up their connections). Alas:
|
|
@@ -366,11 +378,18 @@ async function initialize(event) { // Ensure there is a network promise and map,
|
|
|
366
378
|
}
|
|
367
379
|
document.addEventListener('visibilitychange', initialize);
|
|
368
380
|
window.addEventListener('online', initialize);
|
|
381
|
+
window.addEventListener("appinstalled", async () => {
|
|
382
|
+
// It turns out that the same browser that support this push the EXISTING browsing context into their own frame,
|
|
383
|
+
// rather than starting fresh. (Which makes some sense for having this event fire.)
|
|
384
|
+
// In such cases, we need to re-initialize stuff. We could go point by point, but for now, just hit everything.
|
|
385
|
+
location.reload();
|
|
386
|
+
});
|
|
369
387
|
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
388
|
+
const titleLabel = document.querySelector('#titleLabel');
|
|
389
|
+
titleLabel.textContent = Int([titleLabel.textContent]);
|
|
390
|
+
const titleHostname = document.querySelector('#titleHostname');
|
|
391
|
+
titleHostname.textContent = ` @${location.hostname}`;
|
|
392
|
+
document.querySelector('head > title').innerHTML = titleLabel.textContent + titleHostname.textContent;
|
|
374
393
|
// Set up text for the browser language.
|
|
375
394
|
function initText(selector, content = selector) {
|
|
376
395
|
const element = document.querySelector(selector);
|
|
@@ -380,10 +399,6 @@ function initText(selector, content = selector) {
|
|
|
380
399
|
initText('#aboutReport');
|
|
381
400
|
initText('#aboutShared');
|
|
382
401
|
initText('#aboutFade');
|
|
383
|
-
initText('#aboutAnyone1');
|
|
384
|
-
initText('#aboutAnyone2');
|
|
385
|
-
initText('#aboutAnyone3');
|
|
386
|
-
initText('#learnMore');
|
|
387
402
|
initText('#version');
|
|
388
403
|
initText('#checkForUpdates');
|
|
389
404
|
initText('#downloadUpdates');
|
|
@@ -398,14 +413,33 @@ initText('#describePrivate2');
|
|
|
398
413
|
initText('#describePublic');
|
|
399
414
|
initText('#describeSystem');
|
|
400
415
|
initText('#pickLabels');
|
|
401
|
-
initText('#wipe');
|
|
402
416
|
initText('.firstConversation .teach.correspondent');
|
|
403
417
|
initText('.firstPublish .teach.correspondent');
|
|
404
418
|
initText('.teach.share');
|
|
405
419
|
initText('.teach.attach');
|
|
406
420
|
initText('.teach.changeHashtag');
|
|
407
421
|
|
|
422
|
+
clickTip('#aboutButton', Int`Information about this app, and options to change notifications or how you appear to others.`, event => { // open about
|
|
423
|
+
closeAll();
|
|
424
|
+
openAbout(event);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (isStandalone()) {
|
|
428
|
+
document.querySelector('#wipe').textContent = Int`Installed`;
|
|
429
|
+
} else {
|
|
430
|
+
initText('#wipe');
|
|
431
|
+
clickTip('#wipe', Int`Wipe from ${osName()} all personal data and source files for this app.`, async event => {
|
|
432
|
+
await networkPromise?.then(contact => contact.disconnect());
|
|
433
|
+
localStorage.clear();
|
|
434
|
+
await caches.keys().then(keys => Promise.all(keys.map(key => caches.delete(key))));
|
|
435
|
+
await navigator.serviceWorker.getRegistrations().then(registrations => Promise.all(registrations.map(r => r.unregister())));
|
|
436
|
+
window.location.replace('about/index.html');
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
408
440
|
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('#
|
|
441
|
+
tooltip('#learnNotification', Int`Click to learn about receiving operating system notifications.`);
|
|
442
|
+
tooltip('#learnIdentity', Int`Click to learn about how you can choose to label yourself and how to recognize other users.`);
|
|
443
|
+
tooltip('#learnPWA', Int`Click to learn about installation, updates, and removal.`);
|
|
410
444
|
|
|
411
445
|
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';
|
|
@@ -73,7 +74,7 @@ export function updateLocation(lat, lng, zoom, positionLabel) { // initMap if ne
|
|
|
73
74
|
|
|
74
75
|
export function recenterMap(event) {
|
|
75
76
|
consume(event);
|
|
76
|
-
|
|
77
|
+
closeAll();
|
|
77
78
|
const latLng = [lastLatitude, lastLongitude];
|
|
78
79
|
map.flyTo(latLng);
|
|
79
80
|
}
|
|
@@ -142,9 +143,10 @@ export function initMap(lat, lng, zoom, positionLabel) { // Set up appropriate z
|
|
|
142
143
|
resetInactivityTimer();
|
|
143
144
|
map.closePopup(yourLocation.getPopup());
|
|
144
145
|
});
|
|
145
|
-
map.on('moveend', () => {
|
|
146
|
-
|
|
146
|
+
map.on('moveend', async () => {
|
|
147
|
+
await networkPromise;
|
|
147
148
|
updateLocation(lastLatitude, lastLongitude); // Might now be within map.
|
|
149
|
+
Alert.updateSubscriptions();
|
|
148
150
|
});
|
|
149
151
|
|
|
150
152
|
// 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);
|