@yz-social/civildefense.io 5.0.0 → 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/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": "5.0.0",
4
+ "version": "5.0.3",
5
5
  "keywords": [
6
6
  "map",
7
7
  "browser",
Binary file
Binary file
@@ -16,10 +16,10 @@
16
16
  <header class="hero">
17
17
  <div class="hero-inner">
18
18
  <img class="hero-logo" src="civil-defense.png" alt="Civil Defense Logo">
19
- <h1>The Civil Defense <span>App</span></h1>
20
- <!-- <div class="hero-divider"></div> -->
21
- <!-- <p class="hero-tagline">Realtime crowdmapping that is resistant to tracking, censorship, and takedown</p> -->
19
+ <h1>The Civil Defense <a href="/" target="civildefense"><span>App</span></a></h1>
22
20
  </div>
21
+
22
+
23
23
  <div class="language-selector">
24
24
  <label for="language">Language:</label>
25
25
  <select id="language" name="language" onchange="location = this.value;">
@@ -42,7 +42,7 @@
42
42
  </div>
43
43
 
44
44
  <div class="carousel-slide">
45
- <a href="/?tags=%F0%9F%A9%B7%20community%20%20DEMO,%F0%9F%91%81%EF%B8%8F%20observer%20DEMO,%F0%9F%A7%B0%20utility%20DEMO&lat=44.942473404432015&lng=-93.26251878766926&z=14" target="civildefense"><img src="streets-of-minneapolis.jpg" alt="Map view showing alert markers scattered across a neighborhood"></a>
45
+ <a href="/?tags=%F0%9F%A9%B7%20community%20DEMO,%F0%9F%91%81%EF%B8%8F%20observer%20DEMO,%F0%9F%A7%B0%20utility%20DEMO&lat=44.942473404432015&lng=-93.26251878766926&z=14" target="civildefense"><img src="streets-of-minneapolis.jpg" alt="Map view showing alert markers scattered across a neighborhood"></a>
46
46
  <div class="carousel-caption">Like Twitter, you create the topics.<br>But you only get alerts based on your current map.<br>Like Snapchat, alerts fade away after 24 hours.</div>
47
47
  </div>
48
48
 
@@ -54,6 +54,14 @@
54
54
  </div>
55
55
  </div>
56
56
 
57
+ <div class="carousel-slide">
58
+ <a href="/?tags=%F0%9F%94%A5%20fire,%F0%9F%86%98%20help&lat=48.15529671483125&lng=31.630317514459872&z=6" target="civildefense"><img src="ukraine-fires.jpeg" alt="Map of Ukraine with NASA FIRMS data"></a>
59
+ <div class="carousel-caption">
60
+ <span>Civil Defense can integrate massive amounts of authoritative and crowd-reported data. Here people can report and discuss fires alongside the last 24 hours of NASA satellite data on wildfires, polution fires, and war.</span>
61
+ <a href="/?tags=%F0%9F%94%A5%20fire,%F0%9F%86%98%20help&lat=48.15529671483125&lng=31.630317514459872&z=6" target="civildefense">Show me!</a>
62
+ </div>
63
+ </div>
64
+
57
65
  <div class="carousel-slide">
58
66
  <a href="/?tags=%F0%9F%8E%BC%20classical,%F0%9F%8E%A7%20edm,%F0%9F%8E%A4%20hiphop,%F0%9F%8E%99%EF%B8%8F%20news,%F0%9F%8E%B5%20pop&lat=47.51951505778901&lng=1.6779639902073742&z=6" target="civildefense"><img src="radio-europe.jpg" alt="Map of Western Europe with playable streaming radio station markers"></a>
59
67
  <div class="carousel-caption">
@@ -109,25 +117,32 @@
109
117
  </p>
110
118
 
111
119
  <h2 id="topics">Topics</h2>
112
- <p>Control what topics are shown on the map by simply touching the topic labels to toggle them on and off. Like "hashtags", you add or create new topics by simply typing them - in this case, into the "add topic" box. There is no central list of topics.
113
- <ul>
114
- <li>If you like, you can begin a topic name with an emoji character, and that is what will be used on the map as a marker. Otherwise, a unique shape will be automatically generated and used until you receive an alert for that topic that <i>does</i> have a leading emoji.</li>
115
- <li>You can remove a topic name from your screen altogether by toggling it off, and then touching the "x" that appears. You can add topics back by typing or selecting them from the "add topic" box.</li>
116
- <li>The red "radio button" next to a topic name indicates that it will be the initial topic for a new alert you create by touching the map. (You can also change the topic of an alert you create by touching the topic name in the alert-conversation popup.)</li>
117
- </ul>
118
- </p>
120
+
121
+ <p>Control what topics are shown on the map by simply touching the topic labels to toggle them on and off.</p>
122
+
123
+ <div class="centered">
124
+ <a href="/?tags=%F0%9F%9A%82%20train&highlight=%5Blabel%3D%22%F0%9F%9A%82%20train%22%5D" target="civildefense"><img src="train.png" alt="train topic button"></a>
125
+ </div>
126
+
127
+ <p>You can add or create your own by typing them into the "add topic" box. If your text begins with an emoji, that will be used as the marker in the map.</p>
119
128
 
120
129
  <div class="centered">
121
- <img class="topics" src="topics.png" alt="topic labels">
130
+ <a href="/?highlight=.newtag" target="civildefense"><img src="add-topic.png" alt="add topic box with autocomplete"></a>
122
131
  </div>
123
132
 
133
+ <p>The red "radio button" next to a topic name indicates that it will be the initial topic for a new alert you create by touching the map. You can also change the topic of an alert you create by touching the topic name in the alert-conversation popup.</p>
134
+
135
+ <div class="centered">
136
+ <a href="/?tags=%F0%9F%9A%82%20train&highlight=%5Blabel%3D%22%F0%9F%9A%82%20train%22%5D%20md-icon-button" target="civildefense"><img src="train-radio-button.png" alt="train topic radio button"></a>
137
+ </div>
124
138
 
125
139
  <h2 id="sharing">Sharing</h2>
126
140
 
127
141
  <p>Every alert you make will be immediately visible to anyone with the app who is looking at that point on the map. Every reply you make will be immediately shared with anyone looking at the same alert.</p>
128
142
  <p>You can use the share button to pass a link and message to those that may not yet have the app. The button appears on the map, at the top of an alert-conversation display, and next to each reply, and it shares whatever the button appears with.</p>
143
+
129
144
  <div class="centered">
130
- <img class="button-img" src="../images/share.svg" alt="share">
145
+ <a href="/?highlight=%23share" target="civildefense"><img class="button-img" src="../images/share.svg" alt="share"></a>
131
146
  </div>
132
147
 
133
148
  <h2 id="options">Options</h2>
@@ -135,10 +150,9 @@
135
150
  <p>The app's "<span class="cd">CD</span>" button gives you control over <a href="notification.html">notifications</a>, <a href="identity.html">how you appear to others</a>, and <a href="pwa.html">updates</a>.</p>
136
151
 
137
152
  <div class="hero-inner">
138
- <img class="hero-logo" src="civil-defense.png" alt="Civil Defense Logo">
153
+ <a href="/?highlight=%23aboutButton" target="civildefense"><img class="hero-logo" src="civil-defense.png" alt="Civil Defense Logo"></a>
139
154
  </div>
140
155
 
141
-
142
156
  <!-- <hr class="rule"> -->
143
157
 
144
158
  <h1 id="broadcast">Broadcast Safely</h1>
@@ -146,43 +160,38 @@
146
160
  <h2 id="tracking">Resist Tracking</h2>
147
161
  <ul>
148
162
  <li>You cannot be identified as a person because there is no login, sign-up, or accounts. No phone number or email is used. There is no app store logging downloads. Users can choose to let others recognize them over multiple interactions — without divulging their actual identity.</li>
149
- <li>It is extremely difficult to identify your IP address or network patterns, because alerts are distributed phone-to-phone over a peer-to-peer network.</li>
150
- <li>Notifications are sent directly through the foreground app on your phone, without going through Apple or Google's messaging service. (Backgrounded app notifications available this Winter.)</li>
151
- <li>Each alert disappears everywhere on the network after 24 hours. There are no collected records of messages, comments, or requested topics, and this is not an archive or long-term storage system.</li>
163
+ <li>It is extremely difficult to identify your IP address or network patterns, because alerts are distributed phone-to-phone over a peer-to-peer network.<a href="#p2p"><sup>1</sup></a></li>
164
+ <li>Messages are signed by the sender, sent directly through the network, or through Apple or Google's messaging service without identifying the app, topic, sender, or contents.</li>
165
+ <li>Each alert disappears everywhere on the network after 24 hours. There are no collected records of messages, comments, or requested topics<a href="#p2p"><sup>1</sup></a>, and this is not an archive or long-term storage system.</li>
152
166
  </ul>
153
167
 
154
168
  <h2 id="censorship">Resist Censorship</h2>
155
169
  <ul>
156
170
  <li>No user can be censored because there are no accounts. No one can steal your account information because there isn't any.</li>
157
- <li>No content can be censored, there is no central organization who controls the network or has power to alter content.</li>
171
+ <li>No content can be censored, there is no central organization who controls the network or has power to alter content.<a href="#p2p"><sup>1</sup></a></li>
158
172
  </ul>
159
173
 
160
174
  <h2 id="takedown">Resist Takedown</h2>
161
175
  <ul>
162
176
  <li>No app store can block Civil Defense — the app is just a web page.</li>
163
177
  <li>Taking down web servers or domain names cannot block access for those who already have it, because the app caches everything it needs on first use. The <a href="https://github.com/yz-social/civildefense.io" target="yz.other" rel="noopener">open-source code</a> is available so anyone can host an alternative mirror.</li>
164
- <li>There is no back-end server or data center to take down. All alert data is entirely peer-to-peer. All mirrors share this network: a post on <a href="https://civildefense.io" target="civildefense">civildefense.io</a> appears on the mirror at <a href="https://ki1r0y.com" target="ki1r0y" rel="noopener">ki1r0y.com</a> and vice versa.</li>
165
- <li>Blocking network traffic is extremely difficult, because the low-level data is identical to video conferencing traffic.</li>
166
- <li>Civil Defense is highly resistant to Denial of Service attacks. However, it does require a working Internet, and does not by itself offer any protection against a complete Internet shutdown.</li>
178
+ <li>There is no back-end server or data center to take down. All alert data is entirely peer-to-peer. All mirrors share this network: a post on <a href="https://civildefense.io" target="civildefense">civildefense.io</a> appears on the mirror at <a href="https://ki1r0y.com" target="ki1r0y" rel="noopener">ki1r0y.com</a> and vice versa.<a href="#p2p"><sup>1</sup></a></li>
179
+ <li>Blocking network traffic is extremely difficult, because the low-level data is identical to video conferencing traffic.<a href="#p2p"><sup>1</sup></a></li>
180
+ <li>Civil Defense is highly resistant to Denial of Service attacks.<a href="#p2p"><sup>1</sup></a> However, it does require a working Internet, and does not by itself offer any protection against a complete Internet shutdown.</li>
167
181
  </ul>
168
182
  <!-- <hr class="rule"> -->
169
183
 
170
184
  <h1 id="more">The Bigger Picture</h1>
171
- <p>The Civil Defense runs on the <a href="https://github.com/axona-net/axona-protocol" target="yz.other" rel="noopener">Axona network</a>. Both are free and open source.</p>
185
+ <p>The Civil Defense runs on the <a href="https://github.com/axona-net/axona-protocol" target="yz.other" rel="noopener">Axona network</a><a href="#p2p"><sup>1</sup></a>. Both are free and open source.</p>
172
186
  <p>Axona is a modern descendant of the old <a href="https://en.wikipedia.org/wiki/Peer-to-peer" target="yz.other" rel="noopener">peer-to-peer networks</a> such as <a href="https://en.wikipedia.org/wiki/Napster">Napster</a>. It has a unique self-healing routing mechanism that uses neuron learning techniques to find the best way to distribute alerts. It is analogous to <a href="https://en.wikipedia.org/wiki/Tor_(network)" target="yz.other" rel="noopener">TOR</a> (The Onion Router), but designed from the ground up to run in mobile and desktop browsers.</p>
173
187
  <p>We are also developing a comprehensive quantum-ready cryptography system that will add private-groups for Civil Defense and any software on the network. We believe there are many applications where developers, users, and the public all benefit from open software and secure, decentralized networks — without app stores, servers, or data centers. Beyond Civil Defense, we are also developing large-scale <a href="https://en.wikipedia.org/wiki/Distributed_computing" target="yz.other" rel="noopener">multiuser collaboration</a> and <a href="https://poverty-action.org/inclusive-instant-payment-systems-evidence-based-approach-design-impact" target="yz.other" rel="noopener">Inclusive Instant Payment Systems</a>.</p>
174
188
 
189
+ <hr class="rule">
190
+ <b id="p2p">1</b>: Coming in Fall, 2026
191
+ <hr class="rule">
175
192
 
176
193
  <div class="centered">
177
- <img src="serving-you.jpg" alt="Civil Defense Week poster - Serving you in time of emergency">
178
- </div>
179
-
180
- <div class="centered">
181
- <img src="prevail.jpg" alt="Cold-War Civil Defense poster - Prepare, Protect, Prevail">
182
- </div>
183
-
184
- <div class="centered">
185
- <img src="nazis.png" alt="WWII-style poster exhorting the public to use Civil Defense in case of Nazi attack.">
194
+ <img src="posters.jpeg" alt="Civil Defense posters from the 1950s">
186
195
  </div>
187
196
 
188
197
  </main>
@@ -13,12 +13,27 @@
13
13
  <header class="hero">
14
14
  <div class="hero-inner">
15
15
  <img class="hero-logo" src="civil-defense.png" alt="Civil Defense Logo">
16
- <h1>The Civil Defense <a href="index.html"><span>App</span></a></h1>
16
+ <h1>The <a href="index.html"><span class="about">Civil Defense</span></a> <a href="/" target="civildefense"><span>App</span></a></h1>
17
17
  </div>
18
18
  </header>
19
19
  <main class="content-wrap">
20
20
  <h1>Identity without Accounts</h1>
21
- <p>Lorem ipsum</p>
21
+ <p>There is no signup to use CivilDefense, and no phone number or email is asked for. Instead, each browser or <a href="pwa.html">installation</a> automatically creates a cryptographically secured random identity, and maintains it until you <a href="pwa.html#removal">remove</a> it.</p>
22
+
23
+ <h2>How Others See You</h2>
24
+
25
+ <p>When you create or reply to an <a href="index.html#do">alert</a>, the posting will be shown as "anonymous" with an automatically generated unique image.</p>
26
+ <p>If you like, you can change what will be shown for either of these by touching the "<span class="cd">CD</span>" button and suppling a different "handle" text or "avatar" image. The handle and avatar are how you choose to be recognized, do not have to be unique, and are shared with everyone.</p>
27
+
28
+ <div class="hero-inner">
29
+ <a href="/?highlight=%23aboutButton" target="civildefense"><img class="hero-logo" src="civil-defense.png" alt="Civil Defense Logo"></a>
30
+ </div>
31
+
32
+ <h2>How You See Others</h2>
33
+
34
+ <p>You may find that you want to reliably recognize some contributors over time, even if their chosen handle and avatar are not unique, or if they change their handle or avatar between postings. You can do this by touching <i>their</i> avatar image, and choosing a handle or avatar by which you will recognize them. This choice is never seen by anyone else, so feel free to label them however you need.</p>
35
+
36
+ <hr class="rule">
22
37
 
23
38
  <div class="centered">
24
39
  <img src="prevail.jpg" alt="Cold-War Civil Defense poster - Prepare, Protect, Prevail">
Binary file
@@ -13,12 +13,50 @@
13
13
  <header class="hero">
14
14
  <div class="hero-inner">
15
15
  <img class="hero-logo" src="civil-defense.png" alt="Civil Defense Logo">
16
- <h1>The Civil Defense <a href="index.html"><span>App</span></a></h1>
16
+ <h1>The <a href="index.html"><span class="about">Civil Defense</span></a> <a href="/" target="civildefense"><span>App</span></a></h1>
17
17
  </div>
18
18
  </header>
19
+
19
20
  <main class="content-wrap">
21
+
20
22
  <h1>Notifications without a Feed</h1>
21
- <p>Lorem ipsum</p>
23
+
24
+ <p>When someone else drops an Alert onto the portion of the map you are watching, for a <a href="index.html#topic">topic</a> you are currently displaying, you will see it right away. You can also ask the app to send you a system notification. Touching the notification will open the alert in the app, so that you can see the conversation about it.</p>
25
+
26
+ <p>You must specifically turn this feature on, using the app's "<span class="cd">CD</span>" button. The system will then ask you if you want to enable notifications.
27
+ <ul>
28
+ <li>If you say "yes", you can then turn notifications on and off from the same place in the app.</li>
29
+ <li>If you say "no", or if you later turn off notifications in the browser or system settings, you will have to re-enable it from the browser or system settings. Notifcations on <i>Apple mobile devices</i> are only available when Civil Defense is <a href="pwa.html">installed</a>.</li>
30
+ </ul></p>
31
+
32
+ <div class="hero-inner">
33
+ <a href="/?highlight=%23aboutButton" target="civildefense"><img class="hero-logo" src="civil-defense.png" alt="Civil Defense Logo"></a>
34
+ </div>
35
+
36
+ <p>If you enable notifications, you will also see them:
37
+ <ul>
38
+ <li>If someone else drops an alert near your current position for a topic you are watching, even if you are looking someplace else on the map.</li>
39
+ <li>If someone replies to an alert that you have opened within the current map.</li>
40
+ </ul>
41
+ </p>
42
+
43
+ <h2>Privacy</h2>
44
+
45
+ <p>If the CivilDefense app is running, the notification is made entirely on the same device, without going through any other service.</p>
46
+ <p>However, mobile platforms can be quite aggressive about shutting down apps that you have opened, but which are not currently being displayed on the screen. In this case, the alert must be "pushed" (like a text message) in two steps.:
47
+ <ol>
48
+ <li>First, the CivilDefense network needs to send the message, just as it would if the app were running, but now through a "push service".
49
+ <ul>
50
+ <li>Currently, this is from civildefense.io.</li>
51
+ <li>In the <a href="index.html#broadcast">next major version</a>, this will be from the peer-to-peer network itself.
52
+ </ul>
53
+ Either way, the network encrypts the message and sends it to the push service. It is not identified to the push service as being civildefense.io, but rather with a unique key created just of your most recent Civil Defense app session.
54
+ </li>
55
+ <li>Second, the push server - either Apple or Google - pushes the message to your device. They know that you got a push message, but not the application, topic, sender, or contents.</li>
56
+ </ol>
57
+ </p>
58
+
59
+ <hr class="rule">
22
60
 
23
61
  <div class="centered">
24
62
  <img src="serving-you.jpg" alt="Civil Defense Week poster - Serving you in time of emergency">
Binary file
@@ -13,12 +13,13 @@
13
13
  <header class="hero">
14
14
  <div class="hero-inner">
15
15
  <img class="hero-logo" src="civil-defense.png" alt="Civil Defense Logo">
16
- <h1>The Civil Defense <a href="index.html"><span>App</span></a></h1>
16
+ <h1>The <a href="index.html"><span class="about">Civil Defense</span></a> <a href="/" target="civildefense"><span>App</span></a></h1>
17
17
  </div>
18
18
  </header>
19
- <main class="content-wrap">
19
+
20
+ <main class="content-wrap">
20
21
  <h1>A Real App Without an App Store</h1>
21
- <p><a href="/" target="civildefense">CivilDefense</a> is a full-featured <a href="https://en.wikipedia.org/wiki/Progressive_web_app">app in a web page</a>. However, it does not depend on any app store that can block or remove it, or track who downloaded it. You can run it directly from the <a href="/" target="civildefense">link</a>, without any installation or signup. You can also choose to install it.</p>
22
+ <p><a href="index.html">CivilDefense</a> is a full-featured <a href="https://en.wikipedia.org/wiki/Progressive_web_app">app in a web page</a>. However, it does not depend on any app store that can block or remove it, or track who downloaded it. You can run it directly from the <a href="/" target="civildefense">link</a>, without any installation or signup. You can also choose to install it.</p>
22
23
 
23
24
  <h2 id="installation">Installation</h2>
24
25
  <p>Installing the app makes it easy to run or <a href="#removal">remove</a> it from the home screen or dock a hurry.</p>
@@ -33,8 +34,9 @@
33
34
 
34
35
  <h2 id="updates">Updates</h2>
35
36
  <p>Regardless of whether it is <a href="#installation">installed</a> or run from the browser, the <a href="/" target="civildefense">CivilDefense</a> app automatically keeps a local copy of itself. It periodically checks the original site for new versions, but it does not update without asking you. (You might be in the middle of something or have some other reason to stay on the old version.) The app's "<span class="cd">CD</span>" button also has a "check for updates" button to check immediately.</p>
37
+
36
38
  <div class="hero-inner">
37
- <img class="hero-logo" src="civil-defense.png" alt="Civil Defense Logo">
39
+ <a href="/?highlight=%23aboutButton" target="civildefense"><img class="hero-logo" src="civil-defense.png" alt="Civil Defense Logo"></a>
38
40
  </div>
39
41
 
40
42
  <h2 id="removal">Removal</h2>
@@ -42,7 +44,9 @@
42
44
  Instead, the list of your current active topics and other data are kept only on your own device.
43
45
  No one can see (or steal, or sell) this information without physical access to your device.</p>
44
46
  <p>To remove this local data on an <i><a href="#installation">installed app</a></i>, simply delete or uninstall the app. The operating system will delete all the local data</p>
45
- <p>To remove it from the <i>browser</i>, use the "Wipe all CivilDefense data" link under the app's "<span class="cd">CD</span>" button. On mobile Safari, Apple keeps the browser data separate from the app data, and each must be removed separately. Other platforms share the data between app and browser.</p>
47
+ <p>To remove it from the <i>browser</i>, use the "Wipe all CivilDefense data" link under the app's "<span class="cd">CD</span>" button. On mobile Safari, Apple keeps the browser data separate from the installed-app data, and each must be removed separately. Other platforms share the data between app and browser.</p>
48
+
49
+ <hr class="rule">
46
50
 
47
51
  <div class="centered">
48
52
  <img src="nazis.png" alt="WWII-style poster exhorting the public to use Civil Defense in case of Nazi attack.">
@@ -71,7 +71,11 @@ a:hover { color: var(--red); }
71
71
  margin: 0 auto;
72
72
  }
73
73
  .button-img { width: 80px; }
74
- .topics { max-width: 350px; }
74
+ a > img {
75
+ box-shadow: 0 4px 28px rgba(255, 90, 0, 0.45);
76
+ border-radius: 18px;
77
+ border: solid thick var(--red);
78
+ }
75
79
  .hero-logo {
76
80
  width: 80px;
77
81
  height: 80px;
@@ -90,6 +94,7 @@ a:hover { color: var(--red); }
90
94
  margin-bottom: 0.75rem;
91
95
  }
92
96
  .hero h1 span { color: var(--red-light); }
97
+ .hero h1 span.about { color: var(--white); }
93
98
  .cd {
94
99
  color: var(--red);
95
100
  font-weight: 700;
@@ -158,13 +163,15 @@ a:hover { color: var(--red); }
158
163
  font-size: 18px;
159
164
  line-height: 1.5;
160
165
  text-align: center;
161
- border-top: 2px solid var(--red);
162
166
  min-height: 3.4rem;
163
167
  display: flex;
164
168
  flex-direction: column;
165
169
  align-items: center;
166
170
  justify-content: center;
167
171
  }
172
+ video + .carousel-caption {
173
+ border-top: 2px solid var(--red);
174
+ }
168
175
  /* Prev / Next buttons */
169
176
  .carousel-btn {
170
177
  position: absolute;
@@ -251,7 +258,7 @@ a:hover { color: var(--red); }
251
258
  margin-bottom: 0.9rem;
252
259
  color: var(--body-text);
253
260
  }
254
- .content-wrap ul {
261
+ .content-wrap ul, .content-wrap ol {
255
262
  margin: 0.4rem 0 1rem 1.2rem;
256
263
  }
257
264
  .content-wrap li {
Binary file
Binary file
Binary file
@@ -232,52 +232,86 @@ export class Alert extends Conversation { // A wrapper around L.marker
232
232
  return super.delete();
233
233
  }
234
234
 
235
- static subscriptionQueue = Promise.resolve(); // Serialize updates so they don't overlap each other.
236
- static async updateSubscriptions({newKeys, oldKeys, throttleMS = 20} = {}) { // Update current subscriptions.
237
- // A value of {} passed for oldKeys is used to start things off fresh (i.e., without supressing subscription of any carry-overs).
238
- return this.subscriptionQueue = this.subscriptionQueue.then(async () => {
239
- if (oldKeys && !Object.keys(oldKeys).length) { // This was a reset for a new node.
240
- // Update replies subscriptions for alert that have already been opened.
241
- for (const alert of this.items) {
242
- if (alert.subscribed) await alert.ensureRepliesSubscribed(true);
243
- }
244
- }
245
- oldKeys ||= this.subscriptions;
246
- newKeys ||= this.subscriptionFromMap();
247
-
248
- if (!newKeys) return; // e.g., wacky computation. Don't change anything.
249
- const contact = await networkPromise;
250
- const dropped = [], added = [];
251
- if (!contact) { console.warn("No network through which to subscribe."); return; } // Does this ever happen? Why?
252
- this.subscriptions = newKeys; // Before subscribing.
253
- const subscribe = async (eventName, handler) => {
254
- if (!eventName) console.log('sub to no eventName', {oldKeys, newKeys, dropped, added, handler});
255
- const region = topicRegion(eventName);
256
- const pushData = handler && await this.pushData;
257
- if (handler) Agent.current?.trackPublicChanges(region); // Background. No need to await.
258
- return await contact.subscribe({eventName, region, handler, pushData, pushPersist: null})
259
- .then(sub => {
260
- throttleMS && P2PWebNetwork.delay(throttleMS);
261
- return sub;
262
- });
263
- };
264
- for (const key in newKeys) oldKeys.hasOwnProperty(key) || added.push(key);
265
- for (const key in oldKeys) newKeys.hasOwnProperty(key) || dropped.push(key);
266
- console.log('updating subscriptions', {added, dropped, newKeys, oldKeys});
267
-
268
- // Before subscribing, as that that may bring in an alert with the same tag as one being cleared.
269
- if (this.aggregateLimit) {
270
- // TODO: more efficient way?
271
- new Set(added.map(topicTag)).forEach(tag => {
272
- const hasTag = topicName => topicTag(topicName) === tag;
273
- this.transferOrClearEventMarkers(added.filter(hasTag), dropped.filter(hasTag), newKeys, oldKeys);
274
- });
235
+ static subscriptionQueue = []; // Queue of pending updateSubscriptions reasons.
236
+ static updateSubscriptions(reason = 'map') { // Update current subscriptions for the given purpose:
237
+ // newNode: Supply an empty oldSubscriptions.
238
+ // stickyChange: Make sticky subscriptions match a new state.
239
+ // map: Make fewest changes for new map display.
240
+ //
241
+ // The effects must be serialized, but a pending map update can always be skipped for something after it.
242
+ // We don't want to stack up such changes, because the update can take several seconds on a bad network, during
243
+ // which the user might just keep futzing with the map.
244
+ const queue = this.subscriptionQueue;
245
+ const inProgress = queue.length;
246
+ queue.push(reason);
247
+ if (!inProgress) this.updatePendingSubscriptions();
248
+ }
249
+ static async updatePendingSubscriptions() { // Process the queue.
250
+ const contact = await networkPromise;
251
+ if (!contact) {
252
+ console.warn("No network through which to subscribe.");
253
+ return null; }
254
+
255
+ const queue = this.subscriptionQueue;
256
+ const reason = queue[0];
257
+ let newKeys, oldKeys;
258
+ if (reason === 'stickyChange') {
259
+ await contact.resetPersisted();
260
+ }
261
+ if (reason !== 'map') {
262
+ oldKeys = {};
263
+ // Update replies subscriptions for alerts that have already been opened.
264
+ for (const alert of this.items) {
265
+ if (alert.subscribed) await alert.ensureRepliesSubscribed(true);
275
266
  }
267
+ } else if (queue.length > 1) { // Skip this map and just go on to whatever is pending.
268
+ queue.shift();
269
+ return this.updatePendingSubscriptions();
270
+ }
276
271
 
277
- for (const key of added) await subscribe(key, data => Alert.ensure(data));
278
- for (const key of dropped) await subscribe(key, null);
279
- contact.pushPersist();
280
- });
272
+ // Now complete this subscription update in full, before considering any others.
273
+ oldKeys ||= this.subscriptions;
274
+ newKeys ||= this.subscriptionFromMap();
275
+
276
+ const dropped = [], added = [];
277
+ this.subscriptions = newKeys; // Before subscribing.
278
+ const subscribe = async (eventName, handler) => {
279
+ if (!eventName) console.log('sub to no eventName', {oldKeys, newKeys, dropped, added, handler});
280
+ const region = topicRegion(eventName);
281
+ const pushData = handler && await this.pushData;
282
+ if (handler) Agent.current?.trackPublicChanges(region); // Background. No need to await.
283
+ return contact.subscribe({eventName, region, handler, pushData, pushPersist: null});
284
+ };
285
+ for (const key in newKeys) oldKeys.hasOwnProperty(key) || added.push(key);
286
+ for (const key in oldKeys) newKeys.hasOwnProperty(key) || dropped.push(key);
287
+ console.log('updating subscriptions', {reason, added, dropped, newKeys, oldKeys});
288
+
289
+ // Before subscribing, as that that may bring in an alert with the same tag as one being cleared.
290
+ if (this.aggregateLimit) {
291
+ // TODO: more efficient way?
292
+ new Set(added.map(topicTag)).forEach(tag => {
293
+ const hasTag = topicName => topicTag(topicName) === tag;
294
+ this.transferOrClearEventMarkers(added.filter(hasTag), dropped.filter(hasTag), newKeys, oldKeys);
295
+ });
296
+ }
297
+ // Subtle: await throttle time between initiating network request, but do not wait for each request to complete before starting the next.
298
+ // Do wait for all to complete.
299
+ const throttleMS = 20;
300
+ const promises = [];
301
+ for (const key of added) {
302
+ promises.push(subscribe(key, data => Alert.ensure(data)));
303
+ if (throttleMS) await P2PWebNetwork.delay(throttleMS);
304
+ }
305
+ for (const key of dropped) {
306
+ promises.push(subscribe(key, null));
307
+ if (throttleMS) await P2PWebNetwork.delay(throttleMS);
308
+ }
309
+ contact.pushPersist();
310
+ await Promise.all(promises);
311
+ queue.shift();
312
+ //console.log('updated', promises.length);
313
+ if (queue.length) return this.updatePendingSubscriptions(); // Handle anything now pending.
314
+ return null;
281
315
  }
282
316
  static clearPushData() { // Force new push subscription when next asked. Not needed at startup, but when creating a new node.
283
317
  this._pushData = null; // Otherwise, new subscriptions with the old data will be rejected by the service as being for an unsubscribed client.
@@ -294,11 +328,6 @@ export class Alert extends Conversation { // A wrapper around L.marker
294
328
  resolve({...json, applicationId, applicationServerKey});
295
329
  });
296
330
  }
297
- static async refreshPushSubscriptions() { // Make sticky subscriptions match a new state.
298
- const contact = await networkPromise;
299
- await contact.resetPersisted();
300
- await this.updateSubscriptions({newKeys: this.subscriptions, oldKeys: {}});
301
- }
302
331
 
303
332
  // Instance Management Internals
304
333
  // In general here, a key is an eventName - i.e., a string <mumble>:<cellID>:<hashtag>.
@@ -475,7 +504,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
475
504
  minLng: southWest.lng,
476
505
  maxLng: northEast.lng
477
506
  });
478
- if (!newCells) return null;
507
+ if (!newCells) return {};
479
508
  if (this.shownCells) { // debugging
480
509
  this.shownCells.forEach(polygon => polygon.removeFrom(map));
481
510
  const mapPolys = newCells.map(cell => this.displayCell(cell));
@@ -570,7 +599,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
570
599
 
571
600
  static noMessage = Int`No additional information.`;
572
601
  static closePopup() { // Close any open popup.
573
- map.closePopup();
602
+ map?.closePopup();
574
603
  Hashtags.closeSelector();
575
604
  }
576
605
  static async openPopup(alertTag) { // Open the marker specified by tag.
@@ -794,8 +823,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
794
823
  const ringElement = this.startFader('.alert-commented', remaining);
795
824
  if (ringElement) {
796
825
  ringElement.style.display = 'block';
797
- // Restart the pulse animation by setting animationName to something it isn't.
798
- ringElement.style.animationName = ringElement.style.animationName === 'pulse2' ? 'pulse' : 'pulse2';
826
+ ringElement.style.animationName = 'pulse20 ease 2s 1';
799
827
  }
800
828
  }
801
829
  }
@@ -858,8 +886,8 @@ export class Alert extends Conversation { // A wrapper around L.marker
858
886
  const formatReply = ({tag, payload, hashtag, ...rest}) => {
859
887
  const {message = payload, file, name} = payload || {}; // Message text converts recognized urls to A/V players or links.
860
888
  let text = message
861
- .replace(/https?:\/\/\S+\.(mp3|aac|ogg|oga|opus|m4a|m3u8|m3u|mpu|mpd)$/ig, url => `<audio controls src="${url}" crossorigin="anonymous"></audio>`) // show audio urls as players
862
- .replace(/https?:\/\/\S+\.(mp4|mov|webm)$/ig, url => `<video controls src="${url}" crossorigin="anonymous"></video>`) // show video urls as players
889
+ .replace(/https?:\/\/\S+\.(mp3|aac|ogg|oga|opus|m4a|m3u|mpu|mpd)$/ig, url => `<audio controls src="${url}" crossorigin="anonymous"></audio>`) // show audio urls as players
890
+ .replace(/https?:\/\/\S+\.(mp4|mov|webm|m3u8)$/ig, url => `<video controls src="${url}" crossorigin="anonymous"></video>`) // show video urls as players
863
891
  .replace(/(?<!")https?:\/\/\S+/g, url => `<a href="${url}" target="yz.sidebar">${url}</a>`); // show urls as links
864
892
  let attachment = '';
865
893
  if (file?.startsWith?.('data:image')) attachment = `<a href="${file}" download="${name}"><img class="attachment" src="${file}"></img></a>`;
@@ -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
 
@@ -121,7 +121,7 @@ function noteNotificationPermission(permission) { // Update permission controls/
121
121
  }
122
122
  if (showNotifications.checked === notificationsPreviouslyAllowed) return;
123
123
  notificationsPreviouslyAllowed = showNotifications.checked;
124
- Alert.refreshPushSubscriptions();
124
+ Alert.updateSubscriptions('stickyChange');
125
125
  }
126
126
  clickTip(showNotifications.parentElement, Int`Enable local ${osName()} notifcations for map alerts, without going through any servers. Requires that the app be running.`, event => {
127
127
  resetInactivityTimer();
@@ -192,10 +192,11 @@ document.querySelector('#correspondentContainer md-outlined-text-field').onclick
192
192
  event.stopPropagation();
193
193
  };
194
194
 
195
- clickTip('#share', Int`${osName()} share a link to this map and topics, with picture of map.`, event => {
196
- event.stopPropagation();
195
+ export function shareMap(event) {
196
+ event?.stopPropagation();
197
197
  share({text: "CivilDefense.io", url: getShareableURL().href });
198
- });
198
+ }
199
+ clickTip('#share', Int`${osName()} share a link to this map and topics, with picture of map.`, shareMap);
199
200
 
200
201
  clickTip('#recenterButton', Int`Recenter the map to where you are in the world.`, recenterMap);
201
202
 
@@ -265,7 +266,7 @@ function initializeGeolocation(subscribe = false) { // Arrange to constantly upd
265
266
  if (!subscribeOneShot) return;
266
267
  subscribeOneShot = false;
267
268
  resetInactivityTimer(false);
268
- Alert.updateSubscriptions({oldKeys: {}}); // This was for a new node, so supply an empty oldSubscriptions.
269
+ Alert.updateSubscriptions('newNode');
269
270
  };
270
271
  if (!geolocation) {
271
272
  showMessage(Int`Geolocation not supported. Using default location.`, 'error', 'fail');
@@ -384,6 +385,9 @@ window.addEventListener("appinstalled", async () => {
384
385
  // In such cases, we need to re-initialize stuff. We could go point by point, but for now, just hit everything.
385
386
  location.reload();
386
387
  });
388
+ document.body.addEventListener('animationend', event => {
389
+ event.target.style.animation = '';
390
+ });
387
391
 
388
392
  const titleLabel = document.querySelector('#titleLabel');
389
393
  titleLabel.textContent = Int([titleLabel.textContent]);
@@ -32,9 +32,13 @@ export function showMessage(message, type = 'loading', errorObject) { // Show lo
32
32
  }
33
33
  }
34
34
 
35
+ function flashElement(selector) {
36
+ const element = document.querySelector(selector);
37
+ setTimeout(() => element.style.animation = 'pulse20border ease 2s 1', 100);
38
+ }
39
+
35
40
  let yourLocation; // marker
36
41
  let lastLatitude, lastLongitude;
37
-
38
42
  export function updateLocation(lat, lng, zoom, positionLabel) { // initMap if necessary, and set our position.
39
43
  //console.log('updateLocation', lat, lng, map, yourLocation);
40
44
  // Can't call getCurrentPosition while watching. So set it here for use in recenterMap.
@@ -48,15 +52,16 @@ export function updateLocation(lat, lng, zoom, positionLabel) { // initMap if ne
48
52
  const params = url.searchParams;
49
53
  const tags = params.get('tags');
50
54
  const tagsArray = tags?.split(',') || [];
55
+ const highlight = params.get('highlight');
51
56
  tagsArray.forEach(tag => Hashtags.add(decodeURIComponent(tag)));
52
57
  Hashtags.onchange({resetSubscriptions: false}); // Too early to subscribe, but will be done during initialization.
53
58
  go({lat: params.get('lat'), lng: params.get('lng'), zoom: params.get('z'), alert: params.get('alert')});
54
59
  // We don't need the query parameters now. Get rid of them. They're annoying.
55
60
  if (params.size > 0) {
56
- ['tags', 'lat', 'lng', 'z', 'alert'].forEach(key => params.delete(key));
61
+ ['tags', 'lat', 'lng', 'z', 'alert', 'highlight'].forEach(key => params.delete(key));
57
62
  history.replaceState(null, '', url);
58
63
  }
59
-
64
+ if (highlight) flashElement(highlight);
60
65
  return;
61
66
  }
62
67
  // Otherwise just update the yourLocation marker if appropriate (and not update zoom).
@@ -1,6 +1,6 @@
1
1
  const { Request, Response, URL, clients} = self;
2
2
  // Little point in trying to automatically pull this through package.json, as we need a byte change in THIS file to trigger a new worker.
3
- const serviceVersion = '5.0.0';
3
+ const serviceVersion = '5.0.3';
4
4
 
5
5
  const cacheList = [ // The files we need.
6
6
  "/",
@@ -253,7 +253,7 @@ button, .leaflet-control-zoom > a, .leaflet-popup-close-button, md-outlined-icon
253
253
  }
254
254
  .alert-marker {
255
255
  text-shadow: 8px -3px 10px rgb(0 0 0 / 60%);
256
- animation: pulse ease 0.8s 1;
256
+ animation: pulse08 ease 0.8s 1;
257
257
  transition: opacity 0.4s;
258
258
  }
259
259
  .alert-commented {
@@ -288,7 +288,7 @@ button, .leaflet-control-zoom > a, .leaflet-popup-close-button, md-outlined-icon
288
288
  left: -5px;
289
289
  background-image: radial-gradient(transparent 30%, var(--md-sys-color-primary), transparent 70%);
290
290
  transition: opacity 0.8s;
291
- animation: pulse ease 0.8s 1;
291
+ animation: pulse08 ease 0.8s 1;
292
292
  }
293
293
  .aggregate.alert-pin.starting {
294
294
  font-size: 0px;
@@ -296,19 +296,43 @@ button, .leaflet-control-zoom > a, .leaflet-popup-close-button, md-outlined-icon
296
296
  .aggregate.alert-pin.starting:after {
297
297
  opacity: 0;
298
298
  }
299
- @keyframes pulse {
300
- 0% { filter: saturate(1); }
301
- 25% { filter: saturate(4); }
302
- 50% { filter: saturate(1); }
303
- 75% { filter: saturate(0); }
304
- 100% { filter: saturate(1); }
305
- }
306
- @keyframes pulse2 { /* for restarting */
307
- 0% { filter: saturate(1); }
308
- 25% { filter: saturate(4); }
309
- 50% { filter: saturate(1); }
310
- 75% { filter: saturate(0); }
311
- 100% { filter: saturate(1); }
299
+ @keyframes pulse08 {
300
+ 0% { filter: saturate(1); background-color: rgb(189, 46, 47, 0%); border-radius: 8px; }
301
+ 25% { filter: saturate(4); background-color: rgb(189, 46, 47, 25%); }
302
+ 50% { filter: saturate(1); background-color: rgb(189, 46, 47, 40%); }
303
+ 75% { filter: saturate(0); background-color: rgb(189, 46, 47, 25%); }
304
+ 100% { filter: saturate(1); background-color: rgb(189, 46, 47, 0%); border-radius: 8px;}
305
+ }
306
+ /* 0.2s 0.2s */
307
+ /* <-0.8s-> <-0.8s-> */
308
+ /* 4 4 4 */
309
+ /* 1 1 1 1 1 1 */
310
+ /* 0 0 */
311
+ @keyframes pulse20 {
312
+ 0% { filter: saturate(1); background-color: rgb(189, 46, 47, 0%); border-radius: 8px; }
313
+ 10% { filter: saturate(4); background-color: rgb(189, 46, 47, 25%); }
314
+ 20% { filter: saturate(1); background-color: rgb(189, 46, 47, 50%); }
315
+ 30% { filter: saturate(0); }
316
+ 40% { filter: saturate(1); }
317
+ 50% { filter: saturate(4); }
318
+ 60% { filter: saturate(1); }
319
+ 70% { filter: saturate(0); }
320
+ 80% { filter: saturate(1); background-color: rgb(189, 46, 47, 50%); }
321
+ 90% { filter: saturate(4); background-color: rgb(189, 46, 47, 25%); }
322
+ 100% { filter: saturate(1); background-color: rgb(189, 46, 47, 0%); border-radius: 8px; }
323
+ }
324
+ @keyframes pulse20border {
325
+ 0% { filter: saturate(1); background-color: rgb(189, 46, 47, 0%); border-radius: 8px; border: solid thin rgb(189, 46, 47, 0);}
326
+ 10% { filter: saturate(4); background-color: rgb(189, 46, 47, 25%); border: solid thick rgb(189, 46, 47, 100%); }
327
+ 20% { filter: saturate(1); background-color: rgb(189, 46, 47, 50%); }
328
+ 30% { filter: saturate(0); }
329
+ 40% { filter: saturate(1); }
330
+ 50% { filter: saturate(4); }
331
+ 60% { filter: saturate(1); }
332
+ 70% { filter: saturate(0); }
333
+ 80% { filter: saturate(1); background-color: rgb(189, 46, 47, 50%); }
334
+ 90% { filter: saturate(4); background-color: rgb(189, 46, 47, 25%); border: solid thick rgb(189, 46, 47, 100%); }
335
+ 100% { filter: saturate(1); background-color: rgb(189, 46, 47, 0%); border-radius: 8px; border: solid thin rgb(189, 46, 47, 0); }
312
336
  }
313
337
 
314
338
  #checkForUpdates, #downloadUpdates, #updateContainer md-elevated-button {
@@ -320,7 +344,7 @@ button, .leaflet-control-zoom > a, .leaflet-popup-close-button, md-outlined-icon
320
344
  gap: 10px;
321
345
  }
322
346
  .pub {
323
- animation: pulse ease 0.8s 1;
347
+ animation: pulse08 ease 0.8s 1;
324
348
  --_label-text-weight: 800;
325
349
  --_selected-label-text-color: var(--md-sys-color-secondary);
326
350
  --md-filter-chip-selected-leading-icon-color: var(--md-sys-color-secondary);
@@ -42,7 +42,10 @@ export function configureWebsocket(server) {
42
42
 
43
43
  const interval = setInterval(function ping() { // Keep-alive ping/pong on interval
44
44
  wss.clients.forEach(function each(ws) {
45
- if (!ws.isAlive) return ws.terminate();
45
+ if (!ws.isAlive) {
46
+ console.log('heartbeat failure');
47
+ return ws.terminate();
48
+ }
46
49
  if (ws.readyState == WebSocket.OPEN) {
47
50
  ws.isAlive = false;
48
51
  ws.ping();