@yz-social/civildefense.io 4.5.23 → 4.5.29
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/README.md +1 -1
- package/package.json +1 -1
- package/public/about/checkpoint.jpg +0 -0
- package/public/about/en.html +13 -6
- package/public/about/es.html +62 -40
- package/public/about/radio-europe.jpg +0 -0
- package/public/about/streets-of-minneapolis.jpg +0 -0
- package/public/about/style.css +1 -0
- package/public/index.html +11 -0
- package/public/javascripts/alert.js +52 -30
- package/public/javascripts/hashtags.js +16 -18
- package/public/javascripts/main.js +12 -0
- package/public/javascripts/p2pWebNetwork.js +3 -1
- package/public/javascripts/protocol.js +3 -3
- package/public/javascripts/pubsub.js +18 -17
- package/public/javascripts/translations.js +11 -7
- package/public/javascripts/versions.js +4 -1
- package/public/service-worker.js +3 -2
- package/public/stylesheets/style.css +91 -7
- package/server/getLocation.js +2 -2
- package/server/location.save.json +1 -1
- package/server/websocket.js +6 -4
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ CivilDefense.io lets you report an immediate concern to the public by tapping it
|
|
|
6
6
|
The locations are shared over anonymous p2p with other users in your area, then fade away over 24 hours.
|
|
7
7
|
There is no login and no global tracking of your Internet address or physical location.
|
|
8
8
|
|
|
9
|
-
See [
|
|
9
|
+
See [Overview](https://civildefense.io/about/en.html), [Functional Spec](https://docs.google.com/document/d/1Y621bToMLHyyx6gp4YK8C17jSejMoqpkNQ-R1EZdrEA/edit?usp=sharing), and [Roadmap](https://docs.google.com/document/d/1vwS_oQ6pYw-dn05nsaXi-bda1j-wVVqRdqr7IHnIaf4/edit?usp=sharing).
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
## The Implementation
|
package/package.json
CHANGED
|
Binary file
|
package/public/about/en.html
CHANGED
|
@@ -42,18 +42,25 @@
|
|
|
42
42
|
</div>
|
|
43
43
|
|
|
44
44
|
<div class="carousel-slide">
|
|
45
|
-
<img src="
|
|
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>
|
|
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
|
|
|
49
49
|
<div class="carousel-slide">
|
|
50
|
-
<img src="
|
|
51
|
-
<div class="carousel-caption">
|
|
50
|
+
<a href="/?tags=%F0%9F%91%81%EF%B8%8F%20observer%20DEMO&lat=44.95924556664652&lng=-93.24730055745023&z=16" target="civildefense"><img src="checkpoint.jpg" alt="Map with an open alert popup showing a photo and comment thread"></a>
|
|
51
|
+
<div class="carousel-caption">
|
|
52
|
+
<span>Click a marker to read and add comments and attachments.</span>
|
|
53
|
+
<a href="/?tags=%F0%9F%91%81%EF%B8%8F%20observer%20DEMO&lat=44.95924556664652&lng=-93.24730055745023&z=16" target="civildefense">Show me!</a>
|
|
54
|
+
</div>
|
|
52
55
|
</div>
|
|
53
56
|
|
|
54
57
|
<div class="carousel-slide">
|
|
55
|
-
<img src="
|
|
56
|
-
<div class="carousel-caption">
|
|
58
|
+
<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
|
+
<div class="carousel-caption">
|
|
60
|
+
<span>People control the topics, and they do not have to be about safety.</span>
|
|
61
|
+
<span>Here are playable streaming radio stations by category.</span>
|
|
62
|
+
<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">Show me!</a>
|
|
63
|
+
</div>
|
|
57
64
|
</div>
|
|
58
65
|
|
|
59
66
|
<div class="carousel-slide">
|
|
@@ -94,7 +101,7 @@
|
|
|
94
101
|
<!-- <li>Alerts disappear after 24 hours—this is not an archive or long-term storage system. (This Winter, users will be able to download tamper-evident reports for use in other systems.)</li> -->
|
|
95
102
|
</ul>
|
|
96
103
|
|
|
97
|
-
<a href="/?lang=en" class="launch-btn" target="civildefense">
|
|
104
|
+
<a href="/?tags=%F0%9F%8E%B8%20classic%20rock,%F0%9F%8E%BC%20classical,%F0%9F%A4%A0%20country,%F0%9F%8E%B7%20jazz,%F0%9F%8E%99%EF%B8%8F%20news,%F0%9F%8E%B5%20pop&z=5&lang=en" class="launch-btn" target="civildefense">
|
|
98
105
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
|
|
99
106
|
Try it!
|
|
100
107
|
</a>
|
package/public/about/es.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>La
|
|
6
|
+
<title>La App de Civil Defense</title>
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@400;600;700;800&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
<!-- ── HERO ──────────────────────────────────────────────────── -->
|
|
16
16
|
<header class="hero">
|
|
17
17
|
<div class="hero-inner">
|
|
18
|
-
<img class="hero-logo" src="civil-defense.png" alt="
|
|
19
|
-
<h1>La
|
|
18
|
+
<img class="hero-logo" src="civil-defense.png" alt="Logotipo de Civil Defense">
|
|
19
|
+
<h1>La App de <span>Civil Defense</span></h1>
|
|
20
|
+
<!-- <div class="hero-divider"></div> -->
|
|
21
|
+
<!-- <p class="hero-tagline">Realtime crowdmapping that is resistant to tracking, censorship, and takedown</p> -->
|
|
20
22
|
</div>
|
|
21
23
|
<div class="language-selector">
|
|
22
24
|
<label for="language">Idioma:</label>
|
|
@@ -28,7 +30,7 @@
|
|
|
28
30
|
</header>
|
|
29
31
|
|
|
30
32
|
<!-- ── CAROUSEL ──────────────────────────────────────────────── -->
|
|
31
|
-
<section class="carousel-section" aria-label="Capturas de pantalla de la
|
|
33
|
+
<section class="carousel-section" aria-label="Capturas de pantalla de la app">
|
|
32
34
|
|
|
33
35
|
<div class="carousel-wrap">
|
|
34
36
|
<div class="carousel-track-outer">
|
|
@@ -36,34 +38,50 @@
|
|
|
36
38
|
|
|
37
39
|
<div class="carousel-slide">
|
|
38
40
|
<img src="broadcast.png" alt="Una aplicación móvil mostrando alertas">
|
|
39
|
-
<div class="carousel-caption">Mapeo
|
|
41
|
+
<div class="carousel-caption">Mapeo colectivo en tiempo real, resistente al rastreo, la censura y la eliminación.</div>
|
|
40
42
|
</div>
|
|
41
43
|
|
|
42
44
|
<div class="carousel-slide">
|
|
43
|
-
<img src="
|
|
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="Vista de mapa que muestra marcadores de alerta dispersos por un vecindario"></a>
|
|
44
46
|
<div class="carousel-caption">Como en Twitter, tú creas los temas.<br>Pero solo recibes alertas según tu mapa actual.<br>Como en Snapchat, las alertas desaparecen después de 24 horas.</div>
|
|
45
47
|
</div>
|
|
46
48
|
|
|
47
49
|
<div class="carousel-slide">
|
|
48
|
-
<img src="
|
|
49
|
-
<div class="carousel-caption">
|
|
50
|
+
<a href="/?tags=%F0%9F%91%81%EF%B8%8F%20observer%20DEMO&lat=44.95924556664652&lng=-93.24730055745023&z=16" target="civildefense"><img src="checkpoint.jpg" alt="Mapa con una ventana de alerta abierta que muestra una foto y un hilo de comentarios"></a>
|
|
51
|
+
<div class="carousel-caption">
|
|
52
|
+
<span>Haz clic en un marcador para leer y agregar comentarios y archivos adjuntos.</span>
|
|
53
|
+
<a href="/?tags=%F0%9F%91%81%EF%B8%8F%20observer%20DEMO&lat=44.95924556664652&lng=-93.24730055745023&z=16" target="civildefense">Muéstrame!</a>
|
|
54
|
+
</div>
|
|
50
55
|
</div>
|
|
51
56
|
|
|
52
57
|
<div class="carousel-slide">
|
|
53
|
-
<img src="
|
|
54
|
-
<div class="carousel-caption">
|
|
58
|
+
<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="Mapa de Europa Occidental con marcadores de estaciones de radio en streaming reproducibles"></a>
|
|
59
|
+
<div class="carousel-caption">
|
|
60
|
+
<span>Las personas controlan los temas, y no tienen que tratar sobre seguridad.</span>
|
|
61
|
+
<span>Aquí hay estaciones de radio en streaming reproducibles por categoría.</span>
|
|
62
|
+
<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">Muéstrame!</a>
|
|
63
|
+
</div>
|
|
55
64
|
</div>
|
|
56
65
|
|
|
57
66
|
<div class="carousel-slide">
|
|
58
67
|
<video controls src="CivilDefense.mp4"></video>
|
|
59
|
-
<div class="carousel-caption">Un video de
|
|
68
|
+
<div class="carousel-caption">Un video de 30 segundos con ambientación generada por IA.</div>
|
|
60
69
|
</div>
|
|
61
70
|
|
|
71
|
+
<!-- <div class="carousel-slide"> -->
|
|
72
|
+
<!-- <img src="hero.png" alt="One man with shoping bags in Tiananmen Square, facing an enormous column of tanks."> -->
|
|
73
|
+
<!-- <div class="carousel-caption">Bear witness.</div> -->
|
|
74
|
+
<!-- </div> -->
|
|
75
|
+
|
|
76
|
+
<!-- <div class="carousel-slide"> -->
|
|
77
|
+
<!-- <img src="nazis.png" alt="WWII-style poster exhorting the public to use Civil Defense in case of Nazi attack."> -->
|
|
78
|
+
<!-- </div> -->
|
|
79
|
+
|
|
62
80
|
</div>
|
|
63
81
|
</div>
|
|
64
82
|
|
|
65
83
|
<button class="carousel-btn prev" id="prevBtn" aria-label="Diapositiva anterior">‹</button>
|
|
66
|
-
<button class="carousel-btn next" id="nextBtn" aria-label="
|
|
84
|
+
<button class="carousel-btn next" id="nextBtn" aria-label="Siguiente diapositiva">›</button>
|
|
67
85
|
|
|
68
86
|
<div class="carousel-dots" id="carouselDots" role="tablist" aria-label="Posición del carrusel"></div>
|
|
69
87
|
</div>
|
|
@@ -75,67 +93,71 @@
|
|
|
75
93
|
<h1>Qué Puedes Hacer Con Ella</h1>
|
|
76
94
|
|
|
77
95
|
<ul>
|
|
78
|
-
<li>
|
|
79
|
-
<li>
|
|
80
|
-
<li>
|
|
81
|
-
<li>
|
|
82
|
-
<li>
|
|
96
|
+
<li>Advierte a otros sobre un peligro tocando el mapa.</li>
|
|
97
|
+
<li>Agrega comentarios, fotos, videos u otros archivos adjuntos.</li>
|
|
98
|
+
<li>Recibe notificaciones de nuevas alertas cerca de ti o en tu mapa actual.</li>
|
|
99
|
+
<li>Comparte en redes sociales, mensaje de texto o correo electrónico.</li>
|
|
100
|
+
<li>Discute una alerta mediante videoconferencia segura. (Disponible este otoño.)</li>
|
|
101
|
+
<!-- <li>Alerts disappear after 24 hours—this is not an archive or long-term storage system. (This Winter, users will be able to download tamper-evident reports for use in other systems.)</li> -->
|
|
83
102
|
</ul>
|
|
84
103
|
|
|
85
|
-
<a href="/?lang=es" class="launch-btn" target="civildefense">
|
|
104
|
+
<a href="/?tags=%F0%9F%8E%B8%20classic%20rock,%F0%9F%8E%BC%20classical,%F0%9F%A4%A0%20country,%F0%9F%8E%B7%20jazz,%F0%9F%8E%99%EF%B8%8F%20news,%F0%9F%8E%B5%20pop&z=5&lang=es" class="launch-btn" target="civildefense">
|
|
86
105
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
|
|
87
106
|
¡Pruébala!
|
|
88
107
|
</a>
|
|
89
108
|
|
|
90
|
-
<
|
|
109
|
+
<!-- <hr class="rule"> -->
|
|
110
|
+
|
|
111
|
+
<h1>Difunde de Forma Segura</h1>
|
|
91
112
|
|
|
92
113
|
<h2>Resiste el Rastreo</h2>
|
|
93
114
|
<ul>
|
|
94
|
-
<li>No puedes ser identificado como persona porque no hay inicio de sesión, registro ni cuentas. No se
|
|
95
|
-
<li>Es extremadamente difícil identificar tu dirección IP o
|
|
96
|
-
<li>Las notificaciones se envían directamente a través de la
|
|
97
|
-
<li>Cada alerta desaparece de toda la red después de 24 horas. No se recopilan registros de mensajes, comentarios
|
|
115
|
+
<li>No puedes ser identificado como persona porque no hay inicio de sesión, registro ni cuentas. No se usa ningún número de teléfono ni correo electrónico. Ninguna tienda de aplicaciones registra las descargas. Los usuarios pueden optar por dejar que otros los reconozcan en múltiples interacciones — sin revelar su identidad real.</li>
|
|
116
|
+
<li>Es extremadamente difícil identificar tu dirección IP o tus patrones de red, porque las alertas se distribuyen de teléfono a teléfono a través de una red entre pares (peer-to-peer).</li>
|
|
117
|
+
<li>Las notificaciones se envían directamente a través de la app en primer plano en tu teléfono, sin pasar por el servicio de mensajería de Apple o Google. (Las notificaciones en segundo plano estarán disponibles este invierno.)</li>
|
|
118
|
+
<li>Cada alerta desaparece de toda la red después de 24 horas. No se recopilan registros de mensajes, comentarios o temas solicitados, y este no es un sistema de archivo ni de almacenamiento a largo plazo.</li>
|
|
98
119
|
</ul>
|
|
99
120
|
|
|
100
121
|
<h2>Resiste la Censura</h2>
|
|
101
122
|
<ul>
|
|
102
|
-
<li>Ningún usuario puede ser censurado porque no
|
|
103
|
-
<li>Ningún contenido puede ser censurado; no
|
|
123
|
+
<li>Ningún usuario puede ser censurado porque no existen cuentas. Nadie puede robar la información de tu cuenta porque no la hay.</li>
|
|
124
|
+
<li>Ningún contenido puede ser censurado; no existe una organización central que controle la red o tenga poder para alterar el contenido.</li>
|
|
104
125
|
</ul>
|
|
105
126
|
|
|
106
|
-
<h2>Resiste el
|
|
127
|
+
<h2>Resiste el Cierre</h2>
|
|
107
128
|
<ul>
|
|
108
|
-
<li>Ninguna tienda de aplicaciones puede bloquear
|
|
109
|
-
<li>
|
|
110
|
-
<li>No
|
|
111
|
-
<li>Bloquear el tráfico de red es extremadamente difícil porque los datos de bajo nivel son idénticos al tráfico de
|
|
112
|
-
<li>
|
|
129
|
+
<li>Ninguna tienda de aplicaciones puede bloquear Civil Defense — la app es solo una página web.</li>
|
|
130
|
+
<li>Cerrar servidores web o nombres de dominio no puede bloquear el acceso de quienes ya lo tienen, porque la app almacena en caché todo lo que necesita desde el primer uso. El <a href="https://github.com/yz-social/civildefense.io" target="yz.other" rel="noopener">código de código abierto</a> está disponible para que cualquiera pueda alojar un espejo alternativo.</li>
|
|
131
|
+
<li>No existe un servidor backend ni un centro de datos que se pueda derribar. Todos los datos de las alertas son completamente peer-to-peer. Todos los espejos comparten esta red: una publicación en <a href="https://civildefense.io" target="civildefense">civildefense.io</a> aparece en el espejo <a href="https://ki1r0y.com" target="ki1r0y" rel="noopener">ki1r0y.com</a>, y viceversa.</li>
|
|
132
|
+
<li>Bloquear el tráfico de la red es extremadamente difícil, porque los datos de bajo nivel son idénticos al tráfico de videoconferencias.</li>
|
|
133
|
+
<li>Civil Defense es altamente resistente a los ataques de denegación de servicio (DoS). Sin embargo, sí requiere una conexión a Internet funcional, y por sí sola no ofrece ninguna protección contra un cierre total de Internet.</li>
|
|
113
134
|
</ul>
|
|
135
|
+
<!-- <hr class="rule"> -->
|
|
114
136
|
|
|
115
137
|
<h1>El Panorama General</h1>
|
|
116
|
-
<p>
|
|
117
|
-
<p>Axona es un descendiente moderno de las antiguas <a href="https://en.wikipedia.org/wiki/Peer-to-peer" target="yz.other" rel="noopener">redes
|
|
118
|
-
<p>También estamos desarrollando un sistema
|
|
138
|
+
<p>Civil Defense funciona sobre la red <a href="https://github.com/axona-net/axona-protocol" target="yz.other" rel="noopener">Axona</a>. Ambos son gratuitos y de código abierto.</p>
|
|
139
|
+
<p>Axona es un descendiente moderno de las antiguas <a href="https://en.wikipedia.org/wiki/Peer-to-peer" target="yz.other" rel="noopener">redes peer-to-peer</a>, como <a href="https://en.wikipedia.org/wiki/Napster">Napster</a>. Cuenta con un mecanismo único de enrutamiento autorreparable que utiliza técnicas de aprendizaje neuronal para encontrar la mejor forma de distribuir las alertas. Es análogo a <a href="https://en.wikipedia.org/wiki/Tor_(network)" target="yz.other" rel="noopener">TOR</a> (The Onion Router), pero diseñado desde cero para funcionar en navegadores móviles y de escritorio.</p>
|
|
140
|
+
<p>También estamos desarrollando un completo sistema de criptografía preparado para la era cuántica, que añadirá grupos privados para Civil Defense y para cualquier software de la red. Creemos que existen muchas aplicaciones en las que los desarrolladores, los usuarios y el público en general se benefician de un software abierto y de redes descentralizadas y seguras — sin tiendas de aplicaciones, servidores ni centros de datos. Más allá de Civil Defense, también estamos desarrollando <a href="https://en.wikipedia.org/wiki/Distributed_computing" target="yz.other" rel="noopener">colaboración multiusuario</a> a gran escala y <a href="https://poverty-action.org/inclusive-instant-payment-systems-evidence-based-approach-design-impact" target="yz.other" rel="noopener">Sistemas de Pago Instantáneo Inclusivos</a>.</p>
|
|
141
|
+
|
|
119
142
|
|
|
120
143
|
<div class="centered">
|
|
121
|
-
<img src="serving-you.jpg" alt="Cartel de la Semana de la Defensa Civil
|
|
144
|
+
<img src="serving-you.jpg" alt="Cartel de la Semana de la Defensa Civil - Sirviéndote en tiempos de emergencia">
|
|
122
145
|
</div>
|
|
123
146
|
|
|
124
147
|
<div class="centered">
|
|
125
|
-
<img src="prevail.jpg" alt="
|
|
148
|
+
<img src="prevail.jpg" alt="Cartel de Defensa Civil de la Guerra Fría - Prepárate, Protege, Prevalece">
|
|
126
149
|
</div>
|
|
127
150
|
|
|
128
151
|
<div class="centered">
|
|
129
|
-
<img src="nazis.png" alt="
|
|
152
|
+
<img src="nazis.png" alt="Cartel estilo Segunda Guerra Mundial que exhorta al público a usar la Defensa Civil en caso de ataque nazi.">
|
|
130
153
|
</div>
|
|
131
154
|
|
|
132
155
|
</main>
|
|
133
156
|
|
|
134
|
-
|
|
135
157
|
<!-- ── FOOTER ─────────────────────────────────────────────────── -->
|
|
136
158
|
<footer>
|
|
137
|
-
<p><a href="/?lang=es" target="civildefense">civildefense.io</a> — Mapeo
|
|
138
|
-
|
|
159
|
+
<p><a href="/?lang=es" target="civildefense">civildefense.io</a> — Mapeo colectivo seguro y gratuito para poblaciones vulnerables.<br>
|
|
160
|
+
Agradecemos el uso de software abierto de:
|
|
139
161
|
<a href="https://leafletjs.com/" target="yz.other" rel="noopener">Leaflet</a>,
|
|
140
162
|
<a href="https://www.openstreetmap.org/" target="yz.other" rel="noopener">OpenStreetMap</a>,
|
|
141
163
|
<a href="http://s2geometry.io" target="yz.other" rel="noopener">S2Geometry</a>,
|
|
Binary file
|
|
Binary file
|
package/public/about/style.css
CHANGED
package/public/index.html
CHANGED
|
@@ -161,6 +161,17 @@
|
|
|
161
161
|
|
|
162
162
|
<md-menu positioning="popover" id="popoverMenu"></md-menu>
|
|
163
163
|
<ul class="combobox-listbox hidden" id="knownTagsListbox" role="listbox"></ul>
|
|
164
|
+
|
|
165
|
+
<section class="firstConversation">
|
|
166
|
+
<div class="teach share"></div>
|
|
167
|
+
<div class="teach correspondent"></div>
|
|
168
|
+
<div class="teach attach"></div>
|
|
169
|
+
</section>
|
|
170
|
+
<section class="firstPublish">
|
|
171
|
+
<div class="teach correspondent"></div>
|
|
172
|
+
<div class="teach changeHashtag"></div>
|
|
173
|
+
</section>
|
|
174
|
+
|
|
164
175
|
</section>
|
|
165
176
|
<script type="module" src="javascripts/main.js"></script>
|
|
166
177
|
</body>
|
|
@@ -2,17 +2,20 @@ import * as L from 'leaflet';
|
|
|
2
2
|
import { P2PWebNetwork } from './p2pWebNetwork.js';
|
|
3
3
|
import { Int } from './translations.js';
|
|
4
4
|
import { map, trackMap, showMessage } from './map.js';
|
|
5
|
-
import { networkPromise, resetInactivityTimer, notificationsAllowed, tooltip, clickTip, openAbout, delay, osName } from './main.js';
|
|
5
|
+
import { networkPromise, resetInactivityTimer, notificationsAllowed, tooltip, clickTip, getText, openAbout, delay, osName } from './main.js';
|
|
6
6
|
import { consume } from './display.js';
|
|
7
7
|
import { Hashtags } from './hashtags.js';
|
|
8
8
|
import { Agent } from './agent.js';
|
|
9
9
|
import { Conversation, Reply } from './conversation.js';
|
|
10
|
-
import { alertTopic, topicRegion, cellHex, topicCell } from './versions.js';
|
|
10
|
+
import { alertTopic, topicRegion, cellHex, topicCell, topicTag } from './versions.js';
|
|
11
11
|
import { getContainingCells, getSmallestCellId, getSubdivision, findCoverCellsByMinMaxLatLng, cellContains, pointFromLatLng, cellFromCellID } from './s2.js';
|
|
12
12
|
const { localStorage, getComputedStyle, URL, URLSearchParams, domtoimage } = globalThis;
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
export function getShareableURL(tag = null, tags = Hashtags.getSubscribe()) { // Answer a url that reflects application state.
|
|
16
|
+
// Regardless of whether an alert tag is given, the receiving user is always given a lat/lng:
|
|
17
|
+
// - To position their map with the same coverage as the sender
|
|
18
|
+
// - In case the alert has expired, rolled over, or included in an aggregate.
|
|
16
19
|
const params = new URLSearchParams(location.search);
|
|
17
20
|
const zoom = map.getZoom();
|
|
18
21
|
const { lat, lng } = map.getCenter();
|
|
@@ -118,20 +121,22 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
118
121
|
// Conversation.ensure is the subscription handler, and it keeps track of the instances by tag, initializing a new one if needed.
|
|
119
122
|
initialize({topic, payload, hashtag, tag, agent, issuedTime, ...rest}) { // Make appropriate instance for a new individual tag, or update aggregate.
|
|
120
123
|
|
|
124
|
+
const eventName = topic.name;
|
|
125
|
+
const subscriptions = this.constructor.subscriptions;
|
|
121
126
|
if (!Hashtags.isSubscribed(hashtag)) return null; // A subscribed event may have been in flight while unsubscribing. Caller destroys instance.
|
|
127
|
+
if (!subscriptions.hasOwnProperty(eventName)) return null; // An event may have been in flight while zooming or panning.
|
|
122
128
|
const now = Date.now(),
|
|
123
129
|
expiration = issuedTime + ttl,
|
|
124
130
|
remaining = expiration - now;
|
|
125
131
|
if (remaining < 0) return null; // Network shouldn't send us expired, but if it does, let its instance be destroyed.
|
|
126
132
|
|
|
127
|
-
const eventName = topic.name;
|
|
128
133
|
let keep = this; // Instance to be kept as marker.
|
|
129
134
|
let aggregate = this.constructor.getAggregate(eventName); // If we already have one for this eventName
|
|
130
135
|
|
|
131
136
|
// Each new initialization gets counted, which may be more than the network rollover.
|
|
132
137
|
// We do not "back up" for deletions and expirations - i.e., subtract and possibly go back to individual alerts.
|
|
133
138
|
// Note that initialize() will not be called for an event handler that has a tag (msgId) the same as one we have already seen for a different cell scale.
|
|
134
|
-
|
|
139
|
+
subscriptions[eventName]++;
|
|
135
140
|
|
|
136
141
|
if (aggregate) {
|
|
137
142
|
keep = null; // This new instance is superfluous. Tell ensure() to destroy it...
|
|
@@ -215,20 +220,16 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
215
220
|
console.log('updating subscriptions', {added, dropped, newKeys, oldKeys});
|
|
216
221
|
|
|
217
222
|
// Before subscribing, as that that may bring in an alert with the same tag as one being cleared.
|
|
218
|
-
if (this.aggregateLimit)
|
|
223
|
+
if (this.aggregateLimit) {
|
|
224
|
+
// TODO: more efficient way?
|
|
225
|
+
new Set(added.map(topicTag)).forEach(tag => {
|
|
226
|
+
const hasTag = topicName => topicTag(topicName) === tag;
|
|
227
|
+
this.transferOrClearEventMarkers(added.filter(hasTag), dropped.filter(hasTag), newKeys, oldKeys);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
219
230
|
|
|
220
231
|
for (const key of added) await subscribe(key, data => Alert.ensure(data));
|
|
221
232
|
for (const key of dropped) await subscribe(key, null);
|
|
222
|
-
|
|
223
|
-
// for (const alert of this.items) { // fixme remove
|
|
224
|
-
// if (this.subscriptions[alert.eventName] == undefined) {
|
|
225
|
-
// let kind = added.includes(alert.eventName) && 'added';
|
|
226
|
-
// kind ||= dropped.includes(alert.eventName) && 'dropped';
|
|
227
|
-
// kind ||= Object.keys(newKeys).includes(alert.eventName) && 'new';
|
|
228
|
-
// kind ||= Object.keys(oldKeys).includes(alert.eventName) && 'old';
|
|
229
|
-
// throw new Error(`${kind} ${alert.eventName} has no count after subscription update.`);
|
|
230
|
-
// }
|
|
231
|
-
// }
|
|
232
233
|
});
|
|
233
234
|
}
|
|
234
235
|
|
|
@@ -237,15 +238,16 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
237
238
|
// newKeys/oldKeys are a map of the currently subscribed eventName and the count of events for that cell+hashtag
|
|
238
239
|
static subscriptions = {}; // maps currently active eventNames (<mumble>:<cellID>:<hashtag>) to count of event received for it.
|
|
239
240
|
static aggregateLimit = parseInt(new URLSearchParams(location.search).get('max') || 100); // How many individuals are allowed before we aggregate.
|
|
241
|
+
static MAX_MAP_ZOOM = 19; // If the map is already zoomed to this, there's no place else to go.
|
|
240
242
|
static cellCountOverLimit(eventName, countsDictionary = this.subscriptions) { // Have we received enough that we must show an aggregate?
|
|
241
|
-
return countsDictionary[eventName] >= this.aggregateLimit;
|
|
243
|
+
return (countsDictionary[eventName] >= this.aggregateLimit) && (map.getZoom() < this.MAX_MAP_ZOOM);
|
|
242
244
|
}
|
|
243
245
|
static getAggregate(eventName) { // Answer the existing aggregate for this cell, if any.
|
|
244
246
|
return this.getItem(eventName); // While individual Alerts are stored in items/conversations by tag, the tag for aggregate is the eventName.
|
|
245
247
|
}
|
|
246
248
|
logAlert(label = '') { // For debugging, when an individual or aggregate marker is clicked, show the alert, cell, and count in console.
|
|
247
|
-
const {lat, lng, eventName} = this;
|
|
248
|
-
console.warn(`${label} lat: ${lat}, lng: ${lng}, ${eventName}: ${this.constructor.subscriptions[eventName]}`);
|
|
249
|
+
const {lat, lng, tag, eventName} = this;
|
|
250
|
+
console.warn(`${label} lat: ${lat}, lng: ${lng}, ${eventName}: ${this.constructor.subscriptions[eventName]}, ${tag}`);
|
|
249
251
|
}
|
|
250
252
|
noteEventName(eventName) { // Be a part of the specified grouping.
|
|
251
253
|
if (this.eventName === eventName) return;
|
|
@@ -259,9 +261,10 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
259
261
|
const latLng = s2.LatLng.fromPoint(point);
|
|
260
262
|
return [latLng.lat * radiansToDegrees, latLng.lng * radiansToDegrees];
|
|
261
263
|
});
|
|
262
|
-
const border = this.cellBorder = L.polygon(corners, {color:
|
|
264
|
+
const border = this.cellBorder = L.polygon(corners, {color: this.constructor.md_sys_color_secondary, fillOpacity: 0.1});
|
|
263
265
|
border.addTo(map);
|
|
264
266
|
}
|
|
267
|
+
static md_sys_color_secondary = '#BD2E2F';
|
|
265
268
|
static forEachAlertOf(eventName, callback, alerts = this.items) { // Apply callback to each of alerts matching eventName.
|
|
266
269
|
// TODO? Record a cell's Alert's more efficiently, so that we don't have to cycle through everything.
|
|
267
270
|
alerts.forEach((alert, index, alerts) => (alert.eventName === eventName) && callback(alert, index, alerts));
|
|
@@ -293,8 +296,11 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
293
296
|
setTimeout(() => pin.classList.toggle('starting', false), 100);
|
|
294
297
|
marker.unbindPopup();
|
|
295
298
|
marker.off('click');
|
|
296
|
-
marker.on('click', event =>
|
|
297
|
-
|
|
299
|
+
marker.on('click', event => {
|
|
300
|
+
this.logAlert();
|
|
301
|
+
this.constructor.zoomOn(this.lat, this.lng, hashtag);
|
|
302
|
+
});
|
|
303
|
+
tooltip(element, Int`Zoom in on multiple alerts in this area.`);
|
|
298
304
|
this.noteEventName(this.tag = eventName);
|
|
299
305
|
if (tag !== eventName) {
|
|
300
306
|
networkPromise.then(async contact => contact.subscribe({eventName: tag, region, handler: null})); // Unsubscribe from replies.
|
|
@@ -441,6 +447,13 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
441
447
|
if (this.publishing) { console.log('skiping overlapping publish'); return null; } // do not stack them up.
|
|
442
448
|
try {
|
|
443
449
|
this.publishing = true;
|
|
450
|
+
const cells = getContainingCells(lat, lng);
|
|
451
|
+
const eventNames = cells.map(cell => alertTopic(cell, hashtag));
|
|
452
|
+
if (payload && eventNames.some(eventName => this.getAggregate(eventName))) { // Attemtpt to publish where we are showing an aggregate.
|
|
453
|
+
// There could already be an alert there, so zoom instead.
|
|
454
|
+
this.zoomOn(lat, lng, hashtag);
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
444
457
|
|
|
445
458
|
const contact = await networkPromise; // subtle: The rest of this all happens synchronously, with any null payloads definitely first.
|
|
446
459
|
let oldCells = null, oldHash, oldTag = null; // Recorded for logging, below.
|
|
@@ -468,10 +481,8 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
468
481
|
}
|
|
469
482
|
}
|
|
470
483
|
|
|
471
|
-
const cells = getContainingCells(lat, lng);
|
|
472
484
|
const region = P2PWebNetwork.regionCode(lat, lng);
|
|
473
|
-
for (const
|
|
474
|
-
const eventName = alertTopic(cell, hashtag);
|
|
485
|
+
for (const eventName of eventNames) {
|
|
475
486
|
if (payload) {
|
|
476
487
|
// The Axona message will be {hashtag, issuedTime, payload:{lat, lng, originalPosting}}
|
|
477
488
|
// and when combined with the publisher's authorId will be unique to this user/time/hashtag,
|
|
@@ -498,6 +509,10 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
498
509
|
this.publishing = false;
|
|
499
510
|
}
|
|
500
511
|
}
|
|
512
|
+
static zoomOn(lat, lng, hashtag = '', zoom = map.getZoom() + 1) {
|
|
513
|
+
if (hashtag) showMessage(Int`There are already at least ${this.aggregateLimit} alerts of type "${hashtag}" in the colored region. Zooming in to get a better look.`);
|
|
514
|
+
map.setZoomAround([lat, lng], zoom);
|
|
515
|
+
}
|
|
501
516
|
|
|
502
517
|
static noMessage = Int`No additional information.`;
|
|
503
518
|
static closePopup() { // Close any open popup.
|
|
@@ -547,6 +562,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
547
562
|
|
|
548
563
|
needsRedisplay = true;
|
|
549
564
|
ensureContent(popup = this.marker.getPopup()) { // Set content and handlers in popup if/as needed.
|
|
565
|
+
if (!popup) return;
|
|
550
566
|
if (!popup.isOpen()) return;
|
|
551
567
|
this.logAlert();
|
|
552
568
|
if (!this.needsRedisplay) {
|
|
@@ -562,8 +578,15 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
562
578
|
delay(100).then(() => {
|
|
563
579
|
this.marker.getPopup().update();
|
|
564
580
|
this.initializeHandlers(popup);
|
|
581
|
+
this.teach('firstConversation');
|
|
582
|
+
if (Agent.isMine(this.agent)) this.teach('firstPublish');
|
|
565
583
|
});
|
|
566
584
|
}
|
|
585
|
+
teach(classname) {
|
|
586
|
+
if (localStorage.getItem(classname)) return;
|
|
587
|
+
localStorage.setItem(classname, '1');
|
|
588
|
+
document.body.classList.toggle(classname, true);
|
|
589
|
+
}
|
|
567
590
|
clearAvatars(popup = this.marker?.getPopup()) {
|
|
568
591
|
popup?.getElement()?.querySelectorAll('.correspondent[data-tag]')
|
|
569
592
|
.forEach(element => Agent.ensure({tag: element.dataset.tag}).removeElement(element, 'mixed', element.classList.contains('avatar') ? 'avatar' : 'handle'));
|
|
@@ -574,6 +597,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
574
597
|
const replyButton = replyInput.querySelector('md-filled-icon-button');
|
|
575
598
|
const replyAttachButton = replyInput.querySelector('md-tonal-icon-button');
|
|
576
599
|
const fileChooser = popupElement.querySelector('input[type="file"]');
|
|
600
|
+
replyInput.onfocus = event => resetInactivityTimer();
|
|
577
601
|
replyInput.oninput = event => {
|
|
578
602
|
replyButton.removeAttribute('disabled');
|
|
579
603
|
const input = event.currentTarget;
|
|
@@ -582,7 +606,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
582
606
|
input.rows = internalHighWater;
|
|
583
607
|
};
|
|
584
608
|
clickTip(replyButton, Int`Post your reply.`, event => this.postReply(event));
|
|
585
|
-
clickTip(replyAttachButton,
|
|
609
|
+
clickTip(replyAttachButton, getText('.teach.attach'), event => { resetInactivityTimer(); fileChooser.click(); });
|
|
586
610
|
fileChooser.onchange = event => {
|
|
587
611
|
resetInactivityTimer();
|
|
588
612
|
replyButton.removeAttribute('disabled');
|
|
@@ -597,9 +621,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
597
621
|
const isAvatar = correspondent.classList.contains('avatar');
|
|
598
622
|
if (agent.addElement(correspondent, 'mixed', isAvatar ? 'avatar' : 'handle')) {
|
|
599
623
|
const isMine = Agent.isMine(tag);
|
|
600
|
-
clickTip(correspondent, isMine ?
|
|
601
|
-
Int`Control how others see me.` :
|
|
602
|
-
Int`Control how this person is labeled on my device.`,
|
|
624
|
+
clickTip(correspondent, getText((isMine ? '.firstPublish' : '.firstConversation') + ' .teach.correspondent'),
|
|
603
625
|
event => {
|
|
604
626
|
if (isMine) openAbout(event);
|
|
605
627
|
else agent.describe(event);
|
|
@@ -618,14 +640,14 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
618
640
|
const shareable = popupElement.querySelectorAll('.share');
|
|
619
641
|
for (const element of shareable) clickTip(element, element.closest('.reply') ?
|
|
620
642
|
Int`Share though ${osName()} the text and attachments of this reply, with a link to open this alert.` :
|
|
621
|
-
|
|
643
|
+
getText('.teach.share'), event => this.share(event));
|
|
622
644
|
}
|
|
623
645
|
initChangeHashtag(someParent) { // Init handler on the menu button, if any, as (re-) init of menu for open popup
|
|
624
646
|
const changeHashtag = someParent.querySelector('.changeHashtag');
|
|
625
647
|
if (!changeHashtag) return;
|
|
626
648
|
const menu = document.getElementById('popoverMenu');
|
|
627
649
|
menu.anchorElement = changeHashtag;
|
|
628
|
-
clickTip(changeHashtag,
|
|
650
|
+
clickTip(changeHashtag, getText('.teach.changeHashtag'), event => {
|
|
629
651
|
consume(event);
|
|
630
652
|
menu.open = !menu.open;
|
|
631
653
|
menu.onclick = consume; // Must be onlick rather than addEventListener.
|
|
@@ -76,16 +76,18 @@ export const Hashtags = {
|
|
|
76
76
|
}
|
|
77
77
|
return extended;
|
|
78
78
|
},
|
|
79
|
-
getAll() { // List of all the user's hashtags.
|
|
79
|
+
getAll() { // List of all the user's extended hashtags.
|
|
80
80
|
return Object.keys(this.hashtags);
|
|
81
81
|
},
|
|
82
82
|
getSubscribe() { // Return a list of the hashtags to which the user intendeds to subscribe.
|
|
83
83
|
return this.getAll().filter(tag => this.hashtags[tag]);
|
|
84
84
|
},
|
|
85
|
-
isSubscribed(key) {
|
|
86
|
-
|
|
85
|
+
isSubscribed(key) { // Is this user subscribed to some form of this key.
|
|
86
|
+
const canonical = canonicalTag(key); // no emoji, lower case.
|
|
87
|
+
const ourExtended = this.canonical2extended[canonical]; // our current version, if any
|
|
88
|
+
return this.hashtags[ourExtended];
|
|
87
89
|
},
|
|
88
|
-
isPublish(key) {
|
|
90
|
+
isPublish(key) { // Is this extended tag (in the user's preferred presentation) the user's current publishing tag?
|
|
89
91
|
return this.hashtags[key] === 'pub';
|
|
90
92
|
},
|
|
91
93
|
backupPublisher: false,
|
|
@@ -123,7 +125,7 @@ export const Hashtags = {
|
|
|
123
125
|
if (resetSubscriptions) {
|
|
124
126
|
// We destroy unsubscribed markers right away, because we don't want the user to have to wait and wonder why they're still displayed.
|
|
125
127
|
// If there are alerts in flight, they will be rejected by Alert initialize because we will have already turned off the sub.
|
|
126
|
-
|
|
128
|
+
Alert.items.forEach(wrapper => this.isSubscribed(wrapper.hashtag) || wrapper.destroy());
|
|
127
129
|
Alert.updateSubscriptions();
|
|
128
130
|
}
|
|
129
131
|
},
|
|
@@ -158,6 +160,7 @@ export const Hashtags = {
|
|
|
158
160
|
newtag.removeAttribute('aria-activedescendant');
|
|
159
161
|
newtag.value = '';
|
|
160
162
|
this.activeIndex = -1;
|
|
163
|
+
this.selectors = [];
|
|
161
164
|
},
|
|
162
165
|
openSelector() { // Open the autocomplete tag selector
|
|
163
166
|
const { listbox, newtag } = this;
|
|
@@ -186,8 +189,9 @@ export const Hashtags = {
|
|
|
186
189
|
acceptTag() { // Add the new hashtag.
|
|
187
190
|
resetInactivityTimer();
|
|
188
191
|
let tag = this.newtag?.value // Get into standard form, but do not strip emoji or case into canonical yet.
|
|
189
|
-
.replace(/^#/, '')
|
|
190
|
-
.replace(
|
|
192
|
+
.replace(/^#/, '') // No leading hash
|
|
193
|
+
.replace(':', '.') // Replace colon with some other separator.
|
|
194
|
+
.replace(/\s+/g, ' ') // Replace multiple spaces with a single space
|
|
191
195
|
.normalize('NFD'); // Standardize different ways of making accents into decomposed form - but do not remove them.
|
|
192
196
|
Alert.closePopup();
|
|
193
197
|
if (!tag) return;
|
|
@@ -226,16 +230,8 @@ export const Hashtags = {
|
|
|
226
230
|
item.toLowerCase().includes(matchString)
|
|
227
231
|
);
|
|
228
232
|
|
|
229
|
-
if (matchString) {
|
|
230
|
-
|
|
231
|
-
li.className = 'combobox-empty combobox-option';
|
|
232
|
-
li.textContent = `Create tag "${query}"`;
|
|
233
|
-
li.onpointerdown = event => {
|
|
234
|
-
event.preventDefault();
|
|
235
|
-
event.stopPropagation();
|
|
236
|
-
this.acceptTag();
|
|
237
|
-
};
|
|
238
|
-
listbox.appendChild(li);
|
|
233
|
+
if (matchString && !this.selectors.includes(query)) {
|
|
234
|
+
this.selectors.unshift(query);
|
|
239
235
|
}
|
|
240
236
|
this.selectors.forEach((item, i) => {
|
|
241
237
|
const li = document.createElement('li');
|
|
@@ -321,6 +317,7 @@ export const Hashtags = {
|
|
|
321
317
|
clickTip(newtag, Int`Add a new topic for which the map should show any alerts.`, event => { // Focusing "add topic".
|
|
322
318
|
event.stopPropagation();
|
|
323
319
|
Alert.closePopup();
|
|
320
|
+
resetInactivityTimer();
|
|
324
321
|
this.renderSelector('');
|
|
325
322
|
if (navigator.maxTouchPoints <= 1) { // Only when no multi-touch. On-screen keyboard makes it shoot off the top.
|
|
326
323
|
showMessage(Int`Type a new topic name to see any alerts on the map with this topic.`, 'instructions');
|
|
@@ -360,13 +357,14 @@ export const Hashtags = {
|
|
|
360
357
|
if (this.activeIndex < 0) this.setActive(0);
|
|
361
358
|
else this.selectValue(this.selectors[this.activeIndex]); // Otherwise select what is active.
|
|
362
359
|
break;
|
|
360
|
+
default:
|
|
361
|
+
this.activeIndex = -1;
|
|
363
362
|
}
|
|
364
363
|
};
|
|
365
364
|
newtag.oninput = () => this.renderSelector(newtag.value);
|
|
366
365
|
// When we click on the listbox, the browser will first blur newtag, and then
|
|
367
366
|
// we would not get the click! So here we delay closing a bit.
|
|
368
367
|
newtag.onblur = () => setTimeout(() => this.closeSelector(), 200);
|
|
369
|
-
newtag.onchange = () => this.acceptTag();
|
|
370
368
|
},
|
|
371
369
|
remove(chip, redisplaySubscribers = false) { // Remove this topic, persistently.
|
|
372
370
|
delete this.hashtags[chip.label];
|
|
@@ -24,6 +24,8 @@ var inactivityTimer = null, reconnectCountdown, networkPromise = null;
|
|
|
24
24
|
export { networkPromise };
|
|
25
25
|
export async function resetInactivityTimer(clearMessage = true) { // if !network, initialize(false), else disconnect after INACTIVITY_SECONDSif not restarted
|
|
26
26
|
//console.log('resetInactivityTimer, networkPromise:', networkPromise);
|
|
27
|
+
document.body.classList.toggle('firstConversation', false);
|
|
28
|
+
document.body.classList.toggle('firstPublish', false);
|
|
27
29
|
if (clearMessage) showMessage('');
|
|
28
30
|
clearTimeout(inactivityTimer);
|
|
29
31
|
clearInterval(reconnectCountdown);
|
|
@@ -46,6 +48,11 @@ export function clickTip(elementOrQuerySelector, text, clickHandler) { // toolti
|
|
|
46
48
|
tooltip(element, text);
|
|
47
49
|
element.onclick = clickHandler;
|
|
48
50
|
}
|
|
51
|
+
export function getText(querySelector) {
|
|
52
|
+
const element = document.querySelector(querySelector);
|
|
53
|
+
if (!element) throw new Error(`No element found for getText('${querySelector}').`);
|
|
54
|
+
return element.textContent;
|
|
55
|
+
}
|
|
49
56
|
|
|
50
57
|
function isWebView() { return /CriOS|(WebView|wv|(iPhone|iPod|iPad)(?!.*Safari))/.test(navigator.userAgent); }
|
|
51
58
|
function isApple() { return navigator.platform.startsWith('Mac') || ['iPhone', 'iPad'].includes(navigator.platform); }
|
|
@@ -392,6 +399,11 @@ initText('#describePublic');
|
|
|
392
399
|
initText('#describeSystem');
|
|
393
400
|
initText('#pickLabels');
|
|
394
401
|
initText('#wipe');
|
|
402
|
+
initText('.firstConversation .teach.correspondent');
|
|
403
|
+
initText('.firstPublish .teach.correspondent');
|
|
404
|
+
initText('.teach.share');
|
|
405
|
+
initText('.teach.attach');
|
|
406
|
+
initText('.teach.changeHashtag');
|
|
395
407
|
|
|
396
408
|
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.`);
|
|
397
409
|
tooltip('#aboutAnyone2', Int`Click to see the source code and documentation for serving copies of this app in another tab.`);
|
|
@@ -45,6 +45,8 @@ export class P2PWebNetwork {
|
|
|
45
45
|
onDisconnect: network.detached,
|
|
46
46
|
author: false
|
|
47
47
|
});
|
|
48
|
+
infoLogger ||= null;
|
|
49
|
+
debugLogger ||= null;
|
|
48
50
|
Object.assign(network, {infoLogger, debugLogger, disconnector: disconnect, transport, nodeIdentity, peer});
|
|
49
51
|
|
|
50
52
|
network.info(`Created network node for kernel ${this.kernelVersion} region 0x${this.regionCode(location.lat, location.lng).toString(16)}.`);
|
|
@@ -65,7 +67,7 @@ export class P2PWebNetwork {
|
|
|
65
67
|
async disconnect(debugLogger = this.debugLogger) { // Politely close network connection.
|
|
66
68
|
const health = this.peer.health();
|
|
67
69
|
await this.disconnector();
|
|
68
|
-
if (debugLogger)
|
|
70
|
+
if (debugLogger) this.debug('health at disconnect', health);
|
|
69
71
|
else this.shortHealth(health);
|
|
70
72
|
this.resetStatePromises();
|
|
71
73
|
}
|
|
@@ -97,9 +97,9 @@ if (dht < 1) {
|
|
|
97
97
|
const subHandler = handlers[tag];
|
|
98
98
|
const inFlightResolver = inFlight[tag];
|
|
99
99
|
|
|
100
|
-
if ((!subHandler && !inFlightResolver) || // debug
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
// if ((!subHandler && !inFlightResolver) || // debug
|
|
101
|
+
// ((typeof(subHandler) !== 'function') && (typeof(inFlightResolver) !== 'function')))
|
|
102
|
+
// console.warn('no handler or request', {tag, rest, subHandler, inFlightResolver, handlers, inFlight});
|
|
103
103
|
|
|
104
104
|
if (subHandler) return subHandler(...rest);
|
|
105
105
|
delete inFlight[tag];
|
|
@@ -69,7 +69,7 @@ async function pauseInvoke(...rest) {
|
|
|
69
69
|
await new Promise(resolve => setTimeout(resolve, throttleMS));
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
export
|
|
72
|
+
export function subscribe(topicName, nodeTag, {since = 'all'}) {
|
|
73
73
|
// Axona allows multiple handlers on the same topic, but we don't use that in civildefense, and do not implement it here.
|
|
74
74
|
const topicId = deriveTopicId(topicName);
|
|
75
75
|
const id = uuidv4();
|
|
@@ -77,24 +77,25 @@ export async function subscribe(topicName, nodeTag, {since = 'all'}) {
|
|
|
77
77
|
expire('sub', topicId, id, () => deleteSub(topicId, id), SUBSCRIPTION_TIMEOUT);
|
|
78
78
|
setBucket(data, 'sub', topicId, nodeTag, id);
|
|
79
79
|
if (since) { // invoke handler on any sticky data, but only after we have told client the subscription id.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
80
|
+
setTimeout(async () => {
|
|
81
|
+
let lastEnvelope = null, lastTime = 0;
|
|
82
|
+
for (const envelope of getDataValues('pub', topicId)) {
|
|
83
|
+
switch (since) {
|
|
84
|
+
case 'all':
|
|
85
|
+
await pauseInvoke(nodeTag, id, envelope);
|
|
86
|
+
break;
|
|
87
|
+
case 'latest':
|
|
88
|
+
if (envelope.ts > lastTime) {
|
|
89
|
+
lastTime = envelope.ts;
|
|
90
|
+
lastEnvelope = envelope;
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
default: // Must be a timestamp
|
|
94
|
+
if (envelope.ts === since) await pauseInvoke(nodeTag, id, envelope);
|
|
91
95
|
}
|
|
92
|
-
break;
|
|
93
|
-
default: // Must be a timestamp
|
|
94
|
-
if (envelope.ts === since) await pauseInvoke(nodeTag, id, envelope);
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
if (lastEnvelope) invoke(nodeTag, id, lastEnvelope);
|
|
98
|
+
}, 100);
|
|
98
99
|
}
|
|
99
100
|
return {topicName, topicId, id};
|
|
100
101
|
}
|
|
@@ -77,6 +77,13 @@ const translations = {
|
|
|
77
77
|
['#downloadDefer']: {en: "no, not yet", es: "No, todavía no."},
|
|
78
78
|
['No update at']: {es: "No hay actualizaciones a las"},
|
|
79
79
|
['is available']: {es: "está disponible"},
|
|
80
|
+
['>Zoom in on multiple alerts in this area.']: {es: "Amplía la vista sobre varias alertas en esta zona."},
|
|
81
|
+
|
|
82
|
+
['.firstConversation .teach.correspondent']: {en: "Change how you label this correspondent.", es: "Cambia la forma en que etiquetas a este corresponsal."},
|
|
83
|
+
['.firstPublish .teach.correspondent']: {en: "Change how you label yourself.", es: "Cambia la forma en que te etiquetas."},
|
|
84
|
+
['.teach.attach']: {en: "Attach a file to your reply.", es: "Adjunta un archivo a tu respuesta."},
|
|
85
|
+
['.teach.share']: {en: "Share this alert through other programs.", es: "Comparte esta alerta a través de otros programas."},
|
|
86
|
+
['.teach.changeHashtag']: {en: "Delete your alert or change its topic.", es: "Elimina tu alerta o cambia su tema."},
|
|
80
87
|
|
|
81
88
|
['#describePrivate1']: {en: "Here are your private labels for", es: "Aquí están sus etiquetas privadas para"},
|
|
82
89
|
['#describePrivate2']: {en: "to help you recognize posts from them:", es: "Para ayudarte a reconocer publicaciones de ellas:"},
|
|
@@ -114,17 +121,11 @@ const translations = {
|
|
|
114
121
|
[' share a link to this map and topics, with picture of map.']: {es: " comparte un enlace a este mapa y a los temas, junto con una imagen del mapa."},
|
|
115
122
|
['Recenter the map to where you are in the world.']: {es: "Vuelve a centrar el mapa en tu ubicación actual."},
|
|
116
123
|
['Post your reply.']: {es: "Publica tu respuesta."},
|
|
117
|
-
['Attach a file to your reply.']: {es: "Adjunta un archivo a tu respuesta."},
|
|
118
124
|
['Delete your reply.']: {es: "Elimina tu respuesta."},
|
|
119
|
-
['Change the topic or delete your alert.']: {es: "Cambia el tema o elimina tu alerta."},
|
|
120
125
|
['Check to see if a new version of the app is available.']: {es: "Comprueba si hay una nueva versión de la aplicación disponible."},
|
|
121
126
|
['Update to a new version of this app.']: {es: "Actualiza a una nueva versión de esta aplicación."},
|
|
122
|
-
['Share though .']: {es: "Comparte a través de "},
|
|
123
127
|
[' the text and attachments of this reply, with a link to open this alert.']: {es: " el texto y los archivos adjuntos de esta respuesta, junto con un enlace para abrir esta alerta."},
|
|
124
|
-
['Share through ']: {es: "Comparte un enlace a través de "},
|
|
125
128
|
[' a link to open this alert.']: {es: " para abrir esta alerta."},
|
|
126
|
-
['Control how others see me.']: {es: "Controla cómo me ven los demás."},
|
|
127
|
-
['Control how this person is labeled on my device.']: {es: "Controla cómo se etiqueta a esta persona en mi dispositivo."},
|
|
128
129
|
['Choose the label you want to appear for alerts and replies by this user.']: {es: "Selecciona la etiqueta que quieras que aparezca para las alertas y respuestas de este usuario."},
|
|
129
130
|
["Capture this unique text so that even if the user changes what they share, you'll still see this text for alerts and replies by this user."]: {es: "Captura este texto único para que, aunque el usuario cambie lo que comparte, sigas viendo este texto en las alertas y respuestas de dicho usuario."},
|
|
130
131
|
["Capture this name they are currently sharing, so that even if the user changes what they share, you'll still see this text for alerts and replies by this user."]: {es: "Guarda el nombre que están compartiendo actualmente, de modo que, aunque el usuario cambie lo que comparte, sigas viendo este texto en las alertas y respuestas de dicho usuario."},
|
|
@@ -132,7 +133,10 @@ const translations = {
|
|
|
132
133
|
["Capture this image they are currently sharing, so that even if the user changes what they share, you'll still see this image for alerts and replies by this user."]: {es: "Guarda esta imagen que están compartiendo actualmente, de modo que, aunque el usuario cambie lo que comparte, sigas viendo esta imagen en las alertas y respuestas de dicho usuario."},
|
|
133
134
|
['Choose the picture you want to appear for alerts and replies by this user.']: {es: "Elige la imagen que quieras que aparezca para las alertas y respuestas de este usuario."},
|
|
134
135
|
['Enable local ']: {es: "Habilita notificaciones locales de "},
|
|
135
|
-
[' notifcations for map alerts, without going through any servers. Requires that the app be running.']: {es: " para alertas del mapa, sin pasar por ningún servidor. Requiere que la aplicación esté en ejecución."},
|
|
136
|
+
[' notifcations for map alerts, without going through any servers. Requires that the app be running.']: {es: " para alertas del mapa, sin pasar por ningún servidor. Requiere que la aplicación esté en ejecución."},
|
|
137
|
+
['There are already at least ']: {es: "Ya hay al menos "},
|
|
138
|
+
[' alerts of type "']: {es: ' alertas de tipo "'},
|
|
139
|
+
['" in the colored region. Zooming in to get a better look.']: {es: '" en la región coloreada. Ampliando la imagen para verla mejor.'},
|
|
136
140
|
|
|
137
141
|
|
|
138
142
|
['Too many posts. (5 allowed every 5 minutes.) Removing oldest from this period.']: {es: "Demasiadas publicaciones. (Se permiten 5 cada 5 minutos). Eliminando las más antiguas de este período."},
|
|
@@ -25,8 +25,11 @@ export function cellHex(cellid) { // Convert cellid BigInt to properly padded he
|
|
|
25
25
|
export function alertTopic(cellid, tag) { // Return topic name for public info about specified tag in cellid.
|
|
26
26
|
return `civildefense.io:${dataVersion}:${cellHex(cellid)}:${canonicalTag(tag)}`;
|
|
27
27
|
}
|
|
28
|
+
export function topicTag(topicName) { // Return the hashtag.
|
|
29
|
+
return topicName.slice(topicName.lastIndexOf(':') + 1); // Currently, colon is not allowed in hashtags.
|
|
30
|
+
}
|
|
28
31
|
export function topicCellHex(topicName) { // Return the hex string cell that is baked in to the topic.
|
|
29
|
-
//
|
|
32
|
+
// dataVersion cannot contain a colon.
|
|
30
33
|
return topicName.match(/civildefense.io:[^:]+:([a-z0-9]+):/)[1];
|
|
31
34
|
}
|
|
32
35
|
export function topicCell(topicName) { // Return the BigInt cell that is baked in to the topic.
|
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 = '4.5.
|
|
3
|
+
const serviceVersion = '4.5.29';
|
|
4
4
|
|
|
5
5
|
const cacheList = [ // The files we need.
|
|
6
6
|
"/",
|
|
@@ -129,7 +129,8 @@ async function cacheFirst({request, event}) {
|
|
|
129
129
|
// Put clone of response in cache (so that original can be returned.
|
|
130
130
|
// Tell event to keep worker open while we put it, even though we return response immediately.
|
|
131
131
|
const cache = await caches.open(serviceVersion);
|
|
132
|
-
event.waitUntil(cache.put(request, responseFromNetwork.clone())
|
|
132
|
+
event.waitUntil(cache.put(request, responseFromNetwork.clone())
|
|
133
|
+
.catch(error => console.error(request.url, mime, error)));
|
|
133
134
|
return responseFromNetwork;
|
|
134
135
|
} catch (error) {
|
|
135
136
|
console.error(request.url, error);
|
|
@@ -273,16 +273,16 @@ button, .leaflet-control-zoom > a, .leaflet-popup-close-button, md-outlined-icon
|
|
|
273
273
|
text-align: center;
|
|
274
274
|
}
|
|
275
275
|
.aggregate.alert-pin {
|
|
276
|
-
font-size:
|
|
277
|
-
line-height:
|
|
278
|
-
top:
|
|
279
|
-
left:
|
|
276
|
+
font-size: 40px;
|
|
277
|
+
line-height: 40px;
|
|
278
|
+
top: 0px;
|
|
279
|
+
left: 0px;
|
|
280
280
|
transition: font-size 0.8s;
|
|
281
281
|
}
|
|
282
|
-
.aggregate.alert-pin:
|
|
282
|
+
.aggregate.alert-pin:before {
|
|
283
283
|
content: "";
|
|
284
|
-
width:
|
|
285
|
-
height:
|
|
284
|
+
width: 50px;
|
|
285
|
+
height: 50px;
|
|
286
286
|
position: absolute;
|
|
287
287
|
top: -5px;
|
|
288
288
|
left: -5px;
|
|
@@ -434,9 +434,93 @@ md-menu {
|
|
|
434
434
|
display: flex;
|
|
435
435
|
align-items: center;
|
|
436
436
|
}
|
|
437
|
+
|
|
438
|
+
.teach {
|
|
439
|
+
position: absolute;
|
|
440
|
+
z-index: 1000;
|
|
441
|
+
background: linear-gradient(color-mix(in srgb, var(--md-sys-color-primary), white 40%), var(--md-sys-color-primary));
|
|
442
|
+
color: white;
|
|
443
|
+
padding: 10px;
|
|
444
|
+
border-radius: 12px;
|
|
445
|
+
}
|
|
446
|
+
.changeHashtag {
|
|
447
|
+
anchor-name: --changeHashtag;
|
|
448
|
+
}
|
|
449
|
+
.correspondent.avatar {
|
|
450
|
+
anchor-name: --correspondentAvatar;
|
|
451
|
+
}
|
|
452
|
+
.attribution > .share {
|
|
453
|
+
anchor-name: --share;
|
|
454
|
+
}
|
|
455
|
+
.reply-input [slot="leading-icon"] {
|
|
456
|
+
anchor-name: --attach;
|
|
457
|
+
}
|
|
458
|
+
md-filter-chip[label="🆘 help"], md-filter-chip[label="🆘 ayuda"] {
|
|
459
|
+
anchor-name: --topicToggle;
|
|
460
|
+
}
|
|
461
|
+
.leaflet-popup-content-wrapper {
|
|
462
|
+
anchor-name: --popup;
|
|
463
|
+
}
|
|
464
|
+
body.firstPublish.firstConversation .firstConversation .correspondent,
|
|
465
|
+
body:not(.firstPublish) .firstPublish,
|
|
466
|
+
body:not(.firstConversation) .firstConversation {
|
|
467
|
+
display: none;
|
|
468
|
+
}
|
|
469
|
+
.teach.share {
|
|
470
|
+
position-anchor: --share;
|
|
471
|
+
bottom: calc(anchor(top) + 60px);
|
|
472
|
+
left: anchor(--popup left);
|
|
473
|
+
max-width: min(45vw, 310px);
|
|
474
|
+
}
|
|
475
|
+
.teach.correspondent {
|
|
476
|
+
position-anchor: --correspondentAvatar;
|
|
477
|
+
bottom: calc(anchor(top) + 15px);
|
|
478
|
+
left: anchor(left);
|
|
479
|
+
}
|
|
480
|
+
.teach.changeHashtag {
|
|
481
|
+
position-anchor: --changeHashtag;
|
|
482
|
+
bottom: calc(anchor(top) + 60px);
|
|
483
|
+
right: anchor(--popup right);
|
|
484
|
+
width: min(45vw, 310px);
|
|
485
|
+
}
|
|
486
|
+
.teach.attach {
|
|
487
|
+
position-anchor: --attach;
|
|
488
|
+
top: calc(anchor(bottom) + 15px);
|
|
489
|
+
left: anchor(--popup left);
|
|
490
|
+
background: linear-gradient(var(--md-sys-color-primary), color-mix(in srgb, var(--md-sys-color-primary), white 40%));
|
|
491
|
+
}
|
|
492
|
+
.teach:after {
|
|
493
|
+
content: "";
|
|
494
|
+
position: absolute;
|
|
495
|
+
display: block;
|
|
496
|
+
width: 0;
|
|
497
|
+
border-style: solid;
|
|
498
|
+
border-color: var(--md-sys-color-primary) transparent;
|
|
499
|
+
}
|
|
500
|
+
.teach.share:after {
|
|
501
|
+
bottom: -60px;
|
|
502
|
+
left: 30px;
|
|
503
|
+
border-width: 60px 15px 0;
|
|
504
|
+
}
|
|
505
|
+
.teach.correspondent:after {
|
|
506
|
+
bottom: -13px;
|
|
507
|
+
left: 6px;
|
|
508
|
+
border-width: 15px 15px 0;
|
|
509
|
+
}
|
|
510
|
+
.teach.changeHashtag:after {
|
|
511
|
+
bottom: -60px;
|
|
512
|
+
right: 45px;
|
|
513
|
+
border-width: 60px 15px 0;
|
|
514
|
+
}
|
|
515
|
+
.teach.attach:after {
|
|
516
|
+
top: -15px;
|
|
517
|
+
left: 32px;
|
|
518
|
+
border-width: 0 15px 15px;
|
|
519
|
+
}
|
|
437
520
|
md-outlined-button {
|
|
438
521
|
padding: 10px;
|
|
439
522
|
}
|
|
523
|
+
|
|
440
524
|
/* .attribution > div:last-child > span { */
|
|
441
525
|
/* text-wrap: nowrap; */
|
|
442
526
|
/* } */
|
package/server/getLocation.js
CHANGED
|
@@ -7,10 +7,10 @@ import { resolve } from './dirname.js';
|
|
|
7
7
|
const filename = './location.json';
|
|
8
8
|
export const data = await import(filename, {with: { type: 'json' }})
|
|
9
9
|
.catch(async error => {
|
|
10
|
-
console.log(
|
|
10
|
+
console.log("Unable to get location.json");
|
|
11
11
|
const response = await fetch('https://ipinfo.io/json');
|
|
12
12
|
const string = await response.text();
|
|
13
|
-
console.log(
|
|
13
|
+
console.log("Estimating location as", string);
|
|
14
14
|
await fs.writeFile(resolve(filename), string, 'utf8');
|
|
15
15
|
return {default: JSON.parse(string)};
|
|
16
16
|
});
|
package/server/websocket.js
CHANGED
|
@@ -5,7 +5,7 @@ import * as operator from '../public/javascripts/pubsub.js';
|
|
|
5
5
|
const sockets = {};
|
|
6
6
|
operator.setReceiver((nodeId, id, envelope) => {
|
|
7
7
|
const socket = sockets[nodeId];
|
|
8
|
-
socket
|
|
8
|
+
socket?.send(JSON.stringify([id, envelope]));
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
function heartbeat() {
|
|
@@ -22,7 +22,7 @@ export function configureWebsocket(server) {
|
|
|
22
22
|
ws.on('message', async message => {
|
|
23
23
|
const [id, methodName, ...rest] = JSON.parse(message);
|
|
24
24
|
const result = await operator[methodName](...rest);
|
|
25
|
-
ws.send(JSON.stringify([id, result]));
|
|
25
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify([id, result]));
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
ws.isAlive = true;
|
|
@@ -39,8 +39,10 @@ export function configureWebsocket(server) {
|
|
|
39
39
|
const interval = setInterval(function ping() { // Keep-alive ping/pong on interval
|
|
40
40
|
wss.clients.forEach(function each(ws) {
|
|
41
41
|
if (!ws.isAlive) return ws.terminate();
|
|
42
|
-
ws.
|
|
43
|
-
|
|
42
|
+
if (ws.readyState == WebSocket.OPEN) {
|
|
43
|
+
ws.isAlive = false;
|
|
44
|
+
ws.ping();
|
|
45
|
+
}
|
|
44
46
|
return null;
|
|
45
47
|
});
|
|
46
48
|
}, 20e3);
|