@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 +1 -1
- 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 +41 -32
- package/public/about/identity.html +17 -2
- package/public/about/join.webp +0 -0
- package/public/about/notification.html +40 -2
- package/public/about/posters.jpeg +0 -0
- package/public/about/pwa.html +9 -5
- package/public/about/style.css +10 -3
- 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/javascripts/alert.js +83 -55
- package/public/javascripts/hashtags.js +1 -0
- package/public/javascripts/main.js +9 -5
- package/public/javascripts/map.js +8 -3
- package/public/service-worker.js +1 -1
- package/public/stylesheets/style.css +40 -16
- package/server/websocket.js +4 -1
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/public/about/en.html
CHANGED
|
@@ -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%
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
<
|
|
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
|
|
150
|
-
<li>
|
|
151
|
-
<li>Each alert disappears everywhere on the network after 24 hours. There are no collected records of messages, comments, or requested topics
|
|
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
|
|
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
|
|
165
|
-
<li>Blocking network traffic is extremely difficult, because the low-level data is identical to video conferencing traffic
|
|
166
|
-
<li>Civil Defense is highly resistant to Denial of Service attacks
|
|
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="
|
|
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="
|
|
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>
|
|
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="
|
|
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
|
-
|
|
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
|
package/public/about/pwa.html
CHANGED
|
@@ -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="
|
|
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">
|
|
20
21
|
<h1>A Real App Without an App Store</h1>
|
|
21
|
-
<p><a href="
|
|
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.">
|
package/public/about/style.css
CHANGED
|
@@ -71,7 +71,11 @@ a:hover { color: var(--red); }
|
|
|
71
71
|
margin: 0 auto;
|
|
72
72
|
}
|
|
73
73
|
.button-img { width: 80px; }
|
|
74
|
-
|
|
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
|
|
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 =
|
|
236
|
-
static
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
for (const
|
|
265
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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|
|
|
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>`;
|
|
@@ -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.
|
|
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
|
-
|
|
196
|
-
event
|
|
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(
|
|
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).
|
package/public/service-worker.js
CHANGED
|
@@ -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.
|
|
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:
|
|
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:
|
|
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
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
100% { filter: saturate(1); }
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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:
|
|
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);
|
package/server/websocket.js
CHANGED
|
@@ -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)
|
|
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();
|