@yz-social/civildefense.io 4.4.1
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 +68 -0
- package/announce.js +24 -0
- package/docs/YZ-Brief.pdf +0 -0
- package/index.js +2 -0
- package/movie/camera.jpg +0 -0
- package/movie/movie.js +151 -0
- package/movie/script.js +272 -0
- package/movie/test.js +2 -0
- package/nginx/nginx.conf +83 -0
- package/nginx/yz.social +81 -0
- package/package.json +34 -0
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -0
- package/public/about/CivilDefense.mp4 +0 -0
- package/public/about/In case of Nazis, use CivilDefense.io.png +0 -0
- package/public/about/broadcast.png +0 -0
- package/public/about/civil-defense.png +0 -0
- package/public/about/conversation-and-image-high.png +0 -0
- package/public/about/conversation-and-image.png +0 -0
- package/public/about/en.html +157 -0
- package/public/about/es.html +142 -0
- package/public/about/flood-fire-ice-high.png +0 -0
- package/public/about/flood-fire-ice.png +0 -0
- package/public/about/hero-high.png +0 -0
- package/public/about/hero.png +0 -0
- package/public/about/index.html +8 -0
- package/public/about/nazis.png +0 -0
- package/public/about/script.js +69 -0
- package/public/about/streaming-radio-high.png +0 -0
- package/public/about/streaming-radio.png +0 -0
- package/public/about/style.css +306 -0
- package/public/control-safe-rectangle.html +40 -0
- package/public/favicon.ico +0 -0
- package/public/images/Achtung.png +0 -0
- package/public/images/YZ Owl.png +0 -0
- package/public/images/civil-defense-122.png +0 -0
- package/public/images/civil-defense-192.png +0 -0
- package/public/images/civil-defense-240.png +0 -0
- package/public/images/civil-defense-512.png +0 -0
- package/public/images/civil-defense.png +0 -0
- package/public/images/hero-small.png +0 -0
- package/public/images/qr-scan.svg +2 -0
- package/public/images/qr.png +0 -0
- package/public/images/qr.svg +155 -0
- package/public/images/recenter.svg +5 -0
- package/public/images/share.png +0 -0
- package/public/images/share.svg +2 -0
- package/public/index.html +170 -0
- package/public/javascripts/agent.js +324 -0
- package/public/javascripts/display.js +25 -0
- package/public/javascripts/hashtags.js +205 -0
- package/public/javascripts/main.js +394 -0
- package/public/javascripts/map.js +725 -0
- package/public/javascripts/p2pWebNetwork.js +245 -0
- package/public/javascripts/s2.js +77 -0
- package/public/javascripts/scripting.js +112 -0
- package/public/javascripts/service-manager.js +149 -0
- package/public/javascripts/translations.js +139 -0
- package/public/javascripts/versions.js +23 -0
- package/public/manifest.json +25 -0
- package/public/owl.ico +0 -0
- package/public/platformer.html +52 -0
- package/public/robots.txt +6 -0
- package/public/service-worker.js +218 -0
- package/public/stylesheets/style.css +442 -0
- package/routes/index.js +123 -0
- package/server/app.js +116 -0
- package/server/bridge.js +397 -0
- package/server/dirname.js +15 -0
- package/server/getLocation.js +18 -0
- package/server/identity.js +111 -0
- package/server/location.json +12 -0
- package/spec/axonSpec.gratuitousNameChangeForSignal +246 -0
- package/spec/axonSpec.js +280 -0
- package/spec/axonSpec.jsRemoveThePartAfterJS +252 -0
- package/spec/civildefenseSpec.js +61 -0
- package/spec/pubsubSpec.js +128 -0
- package/spec/support/jasmine.mjs +14 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
|
+
<title>CivilDefense.io</title>
|
|
7
|
+
<meta name="description" property="og:description" content="Report public dangers by tapping a map, with discussion and photos by hashtag. No sign-in or tracking."/>
|
|
8
|
+
<meta name="keywords" content="safety, p2p, dht">
|
|
9
|
+
<meta name="googlebot" content="notranslate">
|
|
10
|
+
<meta name="image" property="og:image" content="https://civildefense.io/images/hero.png" >
|
|
11
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
12
|
+
<meta name="twitter:image" content="https://civildefense.io/images/hero-small.png" />
|
|
13
|
+
|
|
14
|
+
<link rel="author" href="https://yz.social/index.html" />
|
|
15
|
+
<link rel="help" href="https://yz.social/civildefense.html" />
|
|
16
|
+
<link rel="manifest" href="manifest.json" />
|
|
17
|
+
<link rel="shortcut icon" href="favicon.ico" />
|
|
18
|
+
<link rel="stylesheet" href="leaflet/leaflet.css" />
|
|
19
|
+
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
|
|
20
|
+
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
|
21
|
+
<link rel="stylesheet" href="stylesheets/style.css" />
|
|
22
|
+
<script type="importmap">
|
|
23
|
+
{
|
|
24
|
+
"imports": {
|
|
25
|
+
"uuid": "https://unpkg.com/uuid@13.0.0/dist/index.js",
|
|
26
|
+
"@material/web/": "https://esm.run/@material/web/",
|
|
27
|
+
"s2js": "./s2js/s2js.esm.js",
|
|
28
|
+
"bigfloat": "./bigfloat/esm/index.js",
|
|
29
|
+
"leaflet": "./leaflet/leaflet-src.esm.js",
|
|
30
|
+
"minidenticons": "./minidenticons/minidenticons.min.js",
|
|
31
|
+
"@axona/protocol": "./axona-protocol/src/index.js",
|
|
32
|
+
"@axona/protocol/std": "./axona-protocol/std/index.js",
|
|
33
|
+
"@axona/protocol/transport/web/index.js": "./axona-protocol/src/transport/web/index.js",
|
|
34
|
+
"@yz-social/kdht": "./kdht/index.js",
|
|
35
|
+
"@yz-social/webrtc": "./webrtc/index.js",
|
|
36
|
+
"scripting": "./javascripts/scripting.js"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
<script type="module">
|
|
41
|
+
import '@material/web/all.js';
|
|
42
|
+
import {styles as typescaleStyles} from '@material/web/typography/md-typescale-styles.js';
|
|
43
|
+
document.adoptedStyleSheets.push(typescaleStyles.styleSheet);
|
|
44
|
+
</script>
|
|
45
|
+
<script src="pica/pica.min.js"></script>
|
|
46
|
+
<script src="https://unpkg.com/qr-code-styling@1.5.0/lib/qr-code-styling.js"></script>
|
|
47
|
+
<script src="https://unpkg.com/dom-to-image@2.6.0/dist/dom-to-image.min.js"></script>
|
|
48
|
+
</head>
|
|
49
|
+
<body>
|
|
50
|
+
<section data-nosnippet>
|
|
51
|
+
<div id="mapCapture">
|
|
52
|
+
<div id="map"></div>
|
|
53
|
+
<img src="images/civil-defense-240.png" alt="civildefense" class="about-image">
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div id="subPopoverControls"> <!-- Our control, that appear within the map and the marker popups in stack order -->
|
|
57
|
+
|
|
58
|
+
<button id="aboutButton" class="about-button">
|
|
59
|
+
<img src="images/civil-defense-240.png" alt="civildefense" class="about-image"></img>
|
|
60
|
+
</button>
|
|
61
|
+
|
|
62
|
+
<div class="button-container">
|
|
63
|
+
<button id="qrButton">
|
|
64
|
+
<img src="images/qr-scan.svg" alt="qr code" class="button-image">
|
|
65
|
+
</button>
|
|
66
|
+
<button id="share">
|
|
67
|
+
<img src="images/share.svg" alt="share" class="button-image">
|
|
68
|
+
</button>
|
|
69
|
+
<button id="recenterButton">
|
|
70
|
+
<img src="images/recenter.svg" alt="recenter on position" class="button-image">
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<md-chip-set class="watching-hashtags"></md-chip-set>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div id="info" class="info-banner loading"></div>
|
|
78
|
+
|
|
79
|
+
<div id="qrContainer" class="hidden">
|
|
80
|
+
<div class="display-content"></div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div id="aboutContainer" class="hidden">
|
|
84
|
+
<div class="display-content">
|
|
85
|
+
<h1>Civil Defense</h1>
|
|
86
|
+
<ul>
|
|
87
|
+
<li id="aboutReport"></li>
|
|
88
|
+
<li id="aboutShared"></li>
|
|
89
|
+
<li id="aboutFade"></li>
|
|
90
|
+
</ul>
|
|
91
|
+
<p> <a id="learnMore" href="about/index.html" target="yz.sidebar">Learn More</a> </p>
|
|
92
|
+
<p>
|
|
93
|
+
<span id="aboutAnyone1"></span>
|
|
94
|
+
<a id="aboutAnyone2" href="https://github.com/YZ-social/Yz.social" target="yz.sidebar"></a>
|
|
95
|
+
<span id="aboutAnyone3"></span>
|
|
96
|
+
</p>
|
|
97
|
+
<hr>
|
|
98
|
+
<div>
|
|
99
|
+
<label>
|
|
100
|
+
<md-checkbox id="showNotifications" touch-target="wrapper"></md-checkbox>
|
|
101
|
+
<span id="showNotificationsLabel"></span>
|
|
102
|
+
</label>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="handle-avatar">
|
|
105
|
+
<md-outlined-text-field id="myHandle" placeholder="anonymous"></md-outlined-text-field>
|
|
106
|
+
<md-outlined-icon-button id="myAvatar"></md-outlined-icon-button>
|
|
107
|
+
</div>
|
|
108
|
+
<hr>
|
|
109
|
+
<div class="source-updates">
|
|
110
|
+
<md-elevated-button id="checkForUpdates"></md-elevated-button>
|
|
111
|
+
<md-elevated-button id="downloadUpdates" class="hidden"></md-elevated-button>
|
|
112
|
+
<span id="updateStatus"></span>
|
|
113
|
+
</div>
|
|
114
|
+
<p><small>
|
|
115
|
+
<label for="scriptChooser"> <span id="version">Version</span></label>
|
|
116
|
+
<span id="appVersion"></span>,
|
|
117
|
+
kernel: <span id="kernelVersion"></span>.
|
|
118
|
+
</small>
|
|
119
|
+
<small><a id="wipe" href="#"></a></small></p>
|
|
120
|
+
<input id="scriptChooser" type="file" accept="text/javascript"></input>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div id="correspondentContainer" class="hidden">
|
|
125
|
+
<div class="display-content">
|
|
126
|
+
<div class="private-label">
|
|
127
|
+
<div class="describe">
|
|
128
|
+
<span id="describePrivate1"></span>
|
|
129
|
+
<span class="handle"></span>
|
|
130
|
+
<md-icon class="avatar"></md-icon>
|
|
131
|
+
<span id="describePrivate2"></span>
|
|
132
|
+
</div>
|
|
133
|
+
<md-outlined-text-field></md-outlined-text-field>
|
|
134
|
+
<md-outlined-icon-button></md-outlined-icon-button>
|
|
135
|
+
</div>
|
|
136
|
+
<div id="pickLabels"></div>
|
|
137
|
+
<div class="public-label">
|
|
138
|
+
<div id="describePublic" class="describe"></div>
|
|
139
|
+
<md-outlined-button></md-outlined-button>
|
|
140
|
+
<md-outlined-icon-button></md-outlined-icon-button>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="system-label">
|
|
143
|
+
<div id="describeSystem" class="describe"></div>
|
|
144
|
+
<md-outlined-button></md-outlined-button>
|
|
145
|
+
<md-outlined-icon-button></md-outlined-icon-button>
|
|
146
|
+
</div>
|
|
147
|
+
<input type="file" accept="image/*"></input>
|
|
148
|
+
<div>
|
|
149
|
+
<md-text-button id="correspondentCancel">Cancel</md-text-button>
|
|
150
|
+
<md-text-button id="correspondentOK">OK</md-text-button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div id="updateContainer" class="hidden">
|
|
156
|
+
<div class="display-content">
|
|
157
|
+
<h1 id="newVersionHeader">New version available</h1>
|
|
158
|
+
<p id="updateNowQuestion"></p>
|
|
159
|
+
<p id="updateReload"></p>
|
|
160
|
+
<p id="updateDefer"></p>
|
|
161
|
+
<md-elevated-button id="downloadUpdates2"></md-elevated-button>
|
|
162
|
+
<md-elevated-button id="downloadDefer"></md-elevated-button>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<md-menu positioning="popover" id="popoverMenu"></md-menu>
|
|
167
|
+
<script type="module" src="javascripts/main.js"></script>
|
|
168
|
+
</section>
|
|
169
|
+
</body>
|
|
170
|
+
</html>
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
const { localStorage } = globalThis;
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import { minidenticonSvg } from 'minidenticons';
|
|
4
|
+
import { createAuthorIdentity } from '@axona/protocol';
|
|
5
|
+
import { agentTopic, agentPersistKey } from './versions.js';
|
|
6
|
+
import { Int } from './translations.js';
|
|
7
|
+
import { consume, openDisplay } from './display.js';
|
|
8
|
+
import { networkPromise, resetInactivityTimer, clickTip, tooltip } from './main.js';
|
|
9
|
+
import { P2PWebNetwork } from './p2pWebNetwork.js';
|
|
10
|
+
|
|
11
|
+
export class Agent {
|
|
12
|
+
// Tracks what we know of people, and updates avatars and handles representing them.
|
|
13
|
+
|
|
14
|
+
// Agent metadata are always republished with each alert/reply, using alert's region in region/name/author
|
|
15
|
+
// Not republished when the value changes.
|
|
16
|
+
// Next time the agent is used in an alert/reply, it will publish to the region/name/author
|
|
17
|
+
// Any subscribers in that region will see the new value.
|
|
18
|
+
// Subscribers in other regions will not know of the change -- until the user has activity there.
|
|
19
|
+
// A map display might cross across two or more regions. We do not want the same user to appear
|
|
20
|
+
// differently for alerts on the other side of an arbitrary and invisible line.
|
|
21
|
+
// Thus we keep on Agent per persona tag, and have it display the latest info we have.
|
|
22
|
+
// Currently, this is done with a subscription for each region in which we have had
|
|
23
|
+
// a referencing open alert this session, and it remains an active subscription through the session.
|
|
24
|
+
// (We don't want to try to determine when the last reference goes away to unsubscribe, and we
|
|
25
|
+
// don't want to continuously subscribe/unsubscribe when opening and closing alerts.)
|
|
26
|
+
|
|
27
|
+
constructor({tag, identity}) { // Subscribe to public data for tag.
|
|
28
|
+
// Throughout, 'type' is either 'avatar' (indicating an image) or 'handle' (a string).
|
|
29
|
+
|
|
30
|
+
this.identity = identity; // (tag gets carried through the system handle.)
|
|
31
|
+
// The system value for handle and avatar is the same, but it is convenient to
|
|
32
|
+
// represent this as two types, like everything else.
|
|
33
|
+
this.updateValue(tag, 'system', 'handle');
|
|
34
|
+
this.updateValue(tag, 'system', 'avatar');
|
|
35
|
+
// Our private choice for this user is stored locally.
|
|
36
|
+
// But for our own avatar, is the public choice.
|
|
37
|
+
const scope = Agent.isMine(tag) ? 'public' : 'private';
|
|
38
|
+
this.updateFromLocal(scope, 'handle');
|
|
39
|
+
this.updateFromLocal(scope, 'avatar');
|
|
40
|
+
}
|
|
41
|
+
static isMine(tag) { // Is this one of mine?
|
|
42
|
+
return tag === this.tag;
|
|
43
|
+
}
|
|
44
|
+
get tag() { // Retrieved from system handle or avatar.
|
|
45
|
+
return this.values.handle.system;
|
|
46
|
+
}
|
|
47
|
+
localPersistKey(type, tag = this.tag) { // for localStorage of our private data about this Agent.
|
|
48
|
+
return agentPersistKey(type, tag);
|
|
49
|
+
}
|
|
50
|
+
networkPersistKey(type, tag = this.tag) {
|
|
51
|
+
return agentTopic(type, tag);
|
|
52
|
+
}
|
|
53
|
+
updateFromLocal(scope, type, tag = this.tag) { // get value locally, and then update (which may have side-effect)
|
|
54
|
+
const value = localStorage.getItem(this.localPersistKey(type, tag));
|
|
55
|
+
this.updateValue(value, scope, type, false); // Don't publish until we post.
|
|
56
|
+
}
|
|
57
|
+
trackedRegions = {};
|
|
58
|
+
currentRegion = null;
|
|
59
|
+
trackPublicChanges(region) {
|
|
60
|
+
this.currentRegion = region;
|
|
61
|
+
if (this.trackedRegions[region]) return;
|
|
62
|
+
if (Agent.isMine(this.tag)) this.persistPublicMetadata(region);
|
|
63
|
+
networkPromise.then(contact => {
|
|
64
|
+
this.trackedRegions[region] = true;
|
|
65
|
+
const owner = this.tag;
|
|
66
|
+
['handle', 'avatar'].forEach(type => {
|
|
67
|
+
const eventName = this.networkPersistKey(type);
|
|
68
|
+
contact.subscribe({eventName, region, owner, /*since: 'latest',*/ handler: data => this.setPublicData({...data, type})});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async setPublicData(data) { // Subscription to public data has fired. Update value, but do not not re-publish.
|
|
73
|
+
let {payload, subject, type, topic, ts} = data; // fixme remove topic and ts
|
|
74
|
+
// WARNING: IF we chunkify avatars, and we use since:'all', then we need lock out asynchronous
|
|
75
|
+
// decoding of later timestamps, or of null payloads.
|
|
76
|
+
// if (payload && (type === 'avatar')) {
|
|
77
|
+
// const contact = await networkPromise;
|
|
78
|
+
// const {dataURL} = await contact.assembleChunkedDataURL(payload);
|
|
79
|
+
// payload = dataURL;
|
|
80
|
+
// }
|
|
81
|
+
this.updateValue(payload, 'public', type, false);
|
|
82
|
+
if (subject) this.publicMsgId[type] = subject;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static agents = {}; // tag => Agent
|
|
86
|
+
static ensure({tag, identity, region}) { // Answer Agent for tag, creating it if necessary.
|
|
87
|
+
const agent = this.agents[tag] ||= new this({tag, identity});
|
|
88
|
+
if (region) agent.trackPublicChanges(region);
|
|
89
|
+
return agent;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Track values of various types and scope.
|
|
93
|
+
values = {
|
|
94
|
+
handle: {system: null, public: null, private: null, mixed: null},
|
|
95
|
+
avatar: {system: null, public: null, private: null, mixed: null}
|
|
96
|
+
};
|
|
97
|
+
publicMsgId = {}; // maps type => msgId for saved public data of this Agent instance.
|
|
98
|
+
getValue(scope, type) {
|
|
99
|
+
return this.values[type][scope];
|
|
100
|
+
}
|
|
101
|
+
updateValue(value, scope, type, pushPublic = true) { // Updates dependent elements, and if necessary, the mixed values/elements as well.
|
|
102
|
+
if (this.values[type][scope] === value) return;
|
|
103
|
+
|
|
104
|
+
// Persist if private. For public, update locally but do not publish until this agent publishes an alert or reply.
|
|
105
|
+
if (scope === 'private') this.persistPrivate(value, type);
|
|
106
|
+
else if (pushPublic && (scope === 'public')) return this.persistPublic(value, type);
|
|
107
|
+
this.values[type][scope] = value;
|
|
108
|
+
for (const element of this.elements[type][scope]) this.updateElement(element, type, value);
|
|
109
|
+
if (scope === 'mixed') return null;
|
|
110
|
+
// Compute the mixed value:
|
|
111
|
+
// It is the private value if specified.
|
|
112
|
+
// Otherwise is the first non-empty value private, public, and a version of system.
|
|
113
|
+
const vprivate = this.values[type].private;
|
|
114
|
+
const vpublic = this.values[type].public;
|
|
115
|
+
const vsystem = type === 'avatar' ? this.values[type].system : Int`Anonymous`;
|
|
116
|
+
const vmixed = vprivate === '' ? vsystem : (vprivate || vpublic || vsystem);
|
|
117
|
+
return this.updateValue(vmixed, 'mixed', type);
|
|
118
|
+
}
|
|
119
|
+
persistPrivate(value, type) { // Save locally.
|
|
120
|
+
const key = this.localPersistKey(type);
|
|
121
|
+
if (value === null) localStorage.removeItem(key);
|
|
122
|
+
else localStorage.setItem(key, value);
|
|
123
|
+
}
|
|
124
|
+
async persistPublicMetadata(region) { // Publish handle and avatar.
|
|
125
|
+
this.currentRegion = region;
|
|
126
|
+
await Promise.all(['handle', 'avatar'].map(type => this.persistPublic(this.getValue('public', type) || null, type)));
|
|
127
|
+
}
|
|
128
|
+
async persistPublic(value, type) { // Publish (and we will act on subscription).
|
|
129
|
+
const eventName = this.networkPersistKey(type);
|
|
130
|
+
const region = this.currentRegion;
|
|
131
|
+
const owner = this.tag;
|
|
132
|
+
// TODO: set owner as well.
|
|
133
|
+
const contact = await networkPromise;
|
|
134
|
+
if (value) {
|
|
135
|
+
let payload = value;
|
|
136
|
+
// Our downsampling is such that we do not need to chunkify.
|
|
137
|
+
// if (type === 'avatar') {
|
|
138
|
+
// const blob = await P2PWebNetwork.dataURL2blob(value);
|
|
139
|
+
// payload = await contact.chunkifyBlob({blob, region});
|
|
140
|
+
// }
|
|
141
|
+
return contact.publish({eventName, region, owner, payload});
|
|
142
|
+
}
|
|
143
|
+
// For now, until since:'latest' works, supply a null payload and no subject.
|
|
144
|
+
await contact.publish({eventName, region, owner, payload: null});
|
|
145
|
+
// const subject = this.publicMsgId[type];
|
|
146
|
+
// if (!subject) return null; // We have not published a value, so nothing to kill.
|
|
147
|
+
// await contact.publish({eventName, region, owner, subject, payload: null});
|
|
148
|
+
// return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// We represent handles and avatars by inserting stuff into given elements.
|
|
152
|
+
updateElement(element, type, value) {
|
|
153
|
+
Agent[type](element, value === '' ? this.tag : value);
|
|
154
|
+
}
|
|
155
|
+
static handle(element, value) { // Update handle element with value.
|
|
156
|
+
element.textContent = value || '(none)';
|
|
157
|
+
element.value = value || ''; // Hack: handle input[type="text"] as well. Must be property assignment, not attribute.
|
|
158
|
+
}
|
|
159
|
+
static avatar(element, value) { // Update avatar element with value.
|
|
160
|
+
element.innerHTML = value === null ? '(none)' : (value.startsWith('data') ? this.makeImage(value) : Agent.makeIdenticon(value));
|
|
161
|
+
}
|
|
162
|
+
static downsampleResolution = 128; // max height or width
|
|
163
|
+
static makeImage(url) {
|
|
164
|
+
return `<img src="${url}"></img>`;
|
|
165
|
+
}
|
|
166
|
+
static makeIdenticon(tag) {
|
|
167
|
+
return `<minidenticon-svg username="${tag}"></minidenticon-svg>`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Track elements to be updated when something changes.
|
|
171
|
+
elements = {
|
|
172
|
+
handle: {system: new Set(), public: new Set(), private: new Set(), mixed: new Set()},
|
|
173
|
+
avatar: {system: new Set(), public: new Set(), private: new Set(), mixed: new Set()}
|
|
174
|
+
};
|
|
175
|
+
addElement(element, scope, type) { // If not already present, Update element, add it to th set that will stay updated, and return true.
|
|
176
|
+
if (this.elements[type][scope].has(element)) return false;
|
|
177
|
+
this.updateElement(element, type, this.values[type][scope]);
|
|
178
|
+
this.elements[type][scope].add(element);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
removeElement(element, scope, type) { // Remove element from getting updated.
|
|
182
|
+
this.elements[type][scope].delete(element);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
describe(event) { // Describe someone who posted or replied
|
|
186
|
+
const content = openDisplay('correspondentContainer', event);
|
|
187
|
+
const handleSpan = content.querySelector('.handle');
|
|
188
|
+
const avatarSpan = content.querySelector('.avatar');
|
|
189
|
+
const systemHandle = content.querySelector('.system-label md-outlined-button');
|
|
190
|
+
const publicHandle = content.querySelector('.public-label md-outlined-button');
|
|
191
|
+
const privateHandle = content.querySelector('.private-label md-outlined-text-field');
|
|
192
|
+
const systemAvatar = content.querySelector('.system-label md-outlined-icon-button');
|
|
193
|
+
const publicAvatar = content.querySelector('.public-label md-outlined-icon-button');
|
|
194
|
+
const privateAvatar = content.querySelector('.private-label md-outlined-icon-button');
|
|
195
|
+
const fileChooser = content.querySelector('input[type="file"]');
|
|
196
|
+
privateHandle.label = Int`handle`;
|
|
197
|
+
const ok = document.getElementById('correspondentOK');
|
|
198
|
+
const cancel = document.getElementById('correspondentCancel');
|
|
199
|
+
const oldPrivateHandle = this.getValue('private', 'handle');
|
|
200
|
+
const oldPrivateAvatar = this.getValue('private', 'avatar');
|
|
201
|
+
this.addElement(systemHandle, 'system', 'handle');
|
|
202
|
+
this.addElement(publicHandle, 'public', 'handle');
|
|
203
|
+
this.addElement(privateHandle, 'private', 'handle');
|
|
204
|
+
this.addElement(handleSpan, 'mixed', 'handle');
|
|
205
|
+
this.addElement(systemAvatar, 'system', 'avatar');
|
|
206
|
+
this.addElement(publicAvatar, 'public', 'avatar');
|
|
207
|
+
this.addElement(privateAvatar, 'private', 'avatar');
|
|
208
|
+
this.addElement(avatarSpan, 'mixed', 'avatar');
|
|
209
|
+
|
|
210
|
+
content.onclick = consume; // Normally set by openDisplay to close the display, but we don't want that here.
|
|
211
|
+
clickTip(systemHandle, Int`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.`, event => {
|
|
212
|
+
consume(event);
|
|
213
|
+
this.updateValue('', 'private', 'handle');
|
|
214
|
+
});
|
|
215
|
+
clickTip(publicHandle, Int`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.`, event => {
|
|
216
|
+
consume(event);
|
|
217
|
+
this.updateValue(publicHandle.getAttribute('value') || '', 'private', 'handle');
|
|
218
|
+
});
|
|
219
|
+
privateHandle.oninput = event => {
|
|
220
|
+
resetInactivityTimer();
|
|
221
|
+
this.updateValue(privateHandle.value || null, 'private', 'handle');
|
|
222
|
+
};
|
|
223
|
+
clickTip(systemAvatar, Int`Capture this unique image so that even if the user changes what they share, they will still be shown to you with this picture for alerts and replies by this user.`, event => {
|
|
224
|
+
consume(event);
|
|
225
|
+
this.updateValue('', 'private', 'avatar');
|
|
226
|
+
});
|
|
227
|
+
clickTip(publicAvatar, Int`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.`, event => {
|
|
228
|
+
consume(event);
|
|
229
|
+
const dataURL = publicAvatar.firstElementChild?.getAttribute('src');
|
|
230
|
+
this.updateValue(dataURL || '', 'private', 'avatar');
|
|
231
|
+
});
|
|
232
|
+
tooltip(privateHandle, Int`Choose the label you want to appear for alerts and replies by this user.`);
|
|
233
|
+
clickTip(privateAvatar, Int`Choose the picture you want to appear for alerts and replies by this user.`, event => {
|
|
234
|
+
consume(event);
|
|
235
|
+
fileChooser.oncancel = event => {
|
|
236
|
+
consume(event);
|
|
237
|
+
this.updateValue(null, 'private', 'avatar');
|
|
238
|
+
};
|
|
239
|
+
fileChooser.onchange = async event => {
|
|
240
|
+
consume(event);
|
|
241
|
+
if (!fileChooser.files.length) return;
|
|
242
|
+
const blob = await P2PWebNetwork.downsampledBlob({blob: fileChooser.files[0], maxDimension: Agent.downsampleResolution});
|
|
243
|
+
this.updateValue(await P2PWebNetwork.blob2dataURL(blob), 'private', 'avatar');
|
|
244
|
+
console.log('clearing avatar selection');
|
|
245
|
+
};
|
|
246
|
+
fileChooser.click();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
cancel.onclick = () => {
|
|
250
|
+
this.updateValue(oldPrivateHandle, 'private', 'handle');
|
|
251
|
+
this.updateValue(oldPrivateAvatar, 'private', 'avatar');
|
|
252
|
+
ok.click();
|
|
253
|
+
};
|
|
254
|
+
ok.onclick = () => {
|
|
255
|
+
this.removeElement(systemHandle, 'system', 'handle');
|
|
256
|
+
this.removeElement(publicHandle, 'public', 'handle');
|
|
257
|
+
this.removeElement(privateHandle, 'private', 'handle');
|
|
258
|
+
this.removeElement(handleSpan, 'mixed', 'handle');
|
|
259
|
+
this.removeElement(systemAvatar, 'system', 'avatar');
|
|
260
|
+
this.removeElement(publicAvatar, 'public', 'avatar');
|
|
261
|
+
this.removeElement(privateAvatar, 'private', 'avatar');
|
|
262
|
+
this.removeElement(avatarSpan, 'mixed', 'avatar');
|
|
263
|
+
content.parentElement.classList.toggle('hidden', true);
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
static current = null;
|
|
267
|
+
static tag = null;
|
|
268
|
+
static identity = null;
|
|
269
|
+
static switchUser(tag, identity) { // Set/persist/ensure the current user, return Agent
|
|
270
|
+
this.tag = tag; // Before the ensure().
|
|
271
|
+
this.identity = P2PWebNetwork.currentPublishIdentity = identity;
|
|
272
|
+
localStorage.setItem('usertag', this.tag);
|
|
273
|
+
return this.current = this.ensure({tag, identity});
|
|
274
|
+
}
|
|
275
|
+
static async initialize() { // Initialize what the agent needs from the about screen
|
|
276
|
+
let tag = localStorage.getItem('usertag');
|
|
277
|
+
const persistAs = tag || uuidv4(); // Give it SOMETHING to persistAs.
|
|
278
|
+
const myIdentity = await createAuthorIdentity({persistAs});
|
|
279
|
+
if (tag !== persistAs) { // Fix up persistence by moving it to where it need to go.
|
|
280
|
+
// WARNING: This relies on undocumented behavior of createAuthorIdentity().
|
|
281
|
+
const realTag = myIdentity.authorId;
|
|
282
|
+
const dumpedIdentity = localStorage.getItem(persistAs);
|
|
283
|
+
localStorage.setItem(realTag, dumpedIdentity);
|
|
284
|
+
localStorage.removeItem(persistAs);
|
|
285
|
+
tag = realTag;
|
|
286
|
+
}
|
|
287
|
+
const myAgent = Agent.switchUser(tag, myIdentity);
|
|
288
|
+
const myHandle = document.getElementById('myHandle');
|
|
289
|
+
const myAvatar = document.getElementById('myAvatar');
|
|
290
|
+
myAgent.addElement(myHandle, 'public', 'handle');
|
|
291
|
+
myAgent.addElement(myAvatar, 'mixed', 'avatar'); // display the mixed result
|
|
292
|
+
myHandle.label = Int`your handle`;
|
|
293
|
+
clickTip(myHandle, Int`Change the text label for you shown in the conversation for your alerts and replies.`, consume);
|
|
294
|
+
myHandle.onchange = event => {
|
|
295
|
+
resetInactivityTimer();
|
|
296
|
+
const value = myHandle.value || null;
|
|
297
|
+
console.log('set my handle', value);
|
|
298
|
+
myAgent.updateValue(value, 'public', 'handle');
|
|
299
|
+
myAgent.persistPrivate(value, 'handle'); // So that we'll have it next session.
|
|
300
|
+
};
|
|
301
|
+
clickTip(myAvatar, Int`Change the image shown for you in the conversation for your alerts and replies.`, event => {
|
|
302
|
+
consume(event);
|
|
303
|
+
const fileChooser = document.getElementById('correspondentContainer').querySelector('input[type="file"]');
|
|
304
|
+
fileChooser.oncancel = event => {
|
|
305
|
+
consume(event);
|
|
306
|
+
console.log('cancel my avatar');
|
|
307
|
+
myAgent.updateValue(null, 'public', 'avatar');
|
|
308
|
+
myAgent.persistPrivate(null, 'avatar'); // So that we'll have it next session.
|
|
309
|
+
console.log('clearing avatar selection');
|
|
310
|
+
};
|
|
311
|
+
fileChooser.onchange = async event => {
|
|
312
|
+
consume(event);
|
|
313
|
+
if (!fileChooser.files.length) return;
|
|
314
|
+
const blob = await P2PWebNetwork.downsampledBlob({blob: fileChooser.files[0], maxDimension: Agent.downsampleResolution});
|
|
315
|
+
const url = await P2PWebNetwork.blob2dataURL(blob);
|
|
316
|
+
console.log('set my avatar', url.slice(0, 50));
|
|
317
|
+
myAgent.updateValue(url, 'public', 'avatar');
|
|
318
|
+
myAgent.persistPrivate(url, 'avatar'); // So that we'll have it next session.
|
|
319
|
+
};
|
|
320
|
+
fileChooser.click();
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const { pica, FileReader, File, URL } = globalThis;
|
|
2
|
+
import { resetInactivityTimer } from './main.js';
|
|
3
|
+
import { showMessage } from './map.js';
|
|
4
|
+
|
|
5
|
+
export function consume(event) { // i.e., don't close dialogs
|
|
6
|
+
event?.stopPropagation();
|
|
7
|
+
resetInactivityTimer();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function openDisplay(containerIdentifier, event = null, content = undefined) {
|
|
11
|
+
// Open containerIdentifier with close handler attached and optional content, and return content element
|
|
12
|
+
const containerElement = document.getElementById(containerIdentifier);
|
|
13
|
+
consume(event);
|
|
14
|
+
|
|
15
|
+
containerElement.onclick = event => {
|
|
16
|
+
resetInactivityTimer();
|
|
17
|
+
containerElement.classList.toggle('hidden', true);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const contentElement = containerElement.firstElementChild;
|
|
21
|
+
if (content !== undefined) contentElement.innerHTML = content;
|
|
22
|
+
|
|
23
|
+
containerElement.classList.toggle('hidden', false);
|
|
24
|
+
return contentElement;
|
|
25
|
+
}
|