source: Klonkt/src/assets/js/guardian.js@ 9ce14b8

main
Last change on this file since 9ce14b8 was 9ce14b8, checked in by roboburr <roboburr@…>, 5 weeks ago

Een hulpvraag van een oud-ward gaat naar de geschiedenis (shaer-lgo, vervolg)

Bart heeft een hulpvraag staan van iemand die hij niet meer bewaakt, en kwam er
niet vanaf. Terecht: de knop stond er wel, maar de markeerroute eist dat het NOG
je ward is en antwoordt anders met 403. Klikken deed dus niets, zonder uitleg.

HET LOSLAAT-SCHERM BELOOFDE DIT AL LETTERLIJK -- "je krijgt geen hulpvragen meer
van ze". Nieuwe komen inderdaad niet meer binnen, maar wat er al lag bleef in de
lijst staan die om aandacht vraagt. Dit is dus geen nieuwe keuze maar een belofte
die de app al doet en niet nakwam.

NIET ALS AFGEHANDELD. Dat lag voor de hand en is fout: afhandelen is een claim
over een kind waar je niets meer over te zeggen hebt, en die claim wordt ook nog
rondgestuurd naar de guardians die er wel nog zijn. Er is een derde uitkomst en
die leest als wat hij is: niet meer van jou. De vraag zakt naar de geschiedenis,
zonder knoppen, met een regel die zegt dat hun andere guardians er nog zijn.

Dat laatste is geen troost maar een feit: een guardianship eindigt nooit bij de
LAATSTE guardian (3.4 -- dan is het emancipatie). Er blijft altijd iemand over
voor wie de vraag wel open staat, dus dit dooft geen reddingsboei.

Wat er al gebeurd is blijft leesbaar: was je erbij toen je nog guardian was, dan
blijft die oppik met naam staan. Er wordt niets herschreven.

Vier toetsen, drie talen. Suite 615/615.

  • Property mode set to 100644
File size: 46.7 KB
Line 
1/* Guardian PWA client (FEP-633c): renders the dashboard, adopts wards, and
2 manages the guardian push channel. No framework, no inline scripts (CSP).
3 All user-facing text comes from state.strings (server i18n). */
4(function () {
5 'use strict';
6 // A crash here used to fail silently (buttons just do nothing). Surface it on
7 // the page AND the console so the cause is visible instead of "everything hangs".
8 function fatal(msg) {
9 try {
10 var b = document.getElementById('g-fatal') || document.createElement('div');
11 b.id = 'g-fatal'; b.className = 'g-msg err';
12 b.style.cssText = 'display:block;margin:12px 0;padding:10px 14px';
13 b.textContent = 'Guardian: ' + msg;
14 var root = document.querySelector('main') || document.body;
15 if (!b.parentNode && root) root.insertBefore(b, root.firstChild);
16 } catch (e) { /* last resort */ }
17 try { console.error('[guardian]', msg); } catch (e) { /* no console */ }
18 }
19 try {
20 var S = JSON.parse(document.getElementById('guardian-state').textContent || '{}');
21 var T = S.strings || {};
22
23 function el(tag, cls, text) {
24 var n = document.createElement(tag);
25 if (cls) n.className = cls;
26 if (text != null) n.textContent = text;
27 return n;
28 }
29 function handleOf(uri, cached) {
30 if (cached && cached.charAt(0) === '@') return cached; // trust only real @handles
31 try { var u = new URL(uri); return '@' + u.pathname.split('/').filter(Boolean).pop() + '@' + u.host; }
32 catch (e) { return uri; }
33 }
34 // The server hands over a timestamp already formatted in the site's timezone
35 // (Beheer -> Instellingen), the same clock de Krant and Berichten show. The
36 // slice is only a fallback for a row that predates that field: it shows raw
37 // UTC, which is what made a 20:20 call for help read 18:20.
38 function when(item, raw) {
39 if (item && item.when_text) return item.when_text;
40 return String(raw || '').slice(0, 16).replace('T', ' ');
41 }
42 function show(id, on) { document.getElementById(id).hidden = !on; }
43
44 // ── 1. Help requests ───────────────────────────────────────────────────
45 // A call for help is not an alarm: it may well be settled quietly between a
46 // guardian and the child. So the card carries no siren, it just has to be
47 // impossible to miss. It shows up twice on purpose (Robins keuze): the recent
48 // ones across all children at the top, the full history of one child in that
49 // child's panel.
50 var HELP_TOP = 5;
51
52 /**
53 * Wacht deze hulpvraag nog op iemand?
54 *
55 * EEN plek, met opzet. Dit werd eerst op twee plekken los beslist -- de lijst
56 * en de teller op de wardregel -- en toen de lijst gefilterd werd bleef die
57 * teller een afgehandelde vraag meetellen. Een derde plek komt er vast, en dan
58 * hoort hij hier langs te komen.
59 *
60 * Bij TWIJFEL open: alles wat geen expliciete afsluiting draagt telt als
61 * wachtend. De omgekeerde fout -- iets als afgehandeld tonen dat het niet is --
62 * is hier de gevaarlijke.
63 */
64 function helpOpen(h) { return !h || !h.state || (h.state.open && !h.state.formerWard); }
65
66 function helpCard(h) {
67 var card = el('div', 'g-card help');
68 var row = el('div', 'row');
69 var who = el('span', 'who grow');
70 // name_html carries the custom emojis (FEP-9098) of the display name, the
71 // same way de Krant renders a byline. Falls back to the plain name.
72 if (h.name_html) who.innerHTML = h.name_html;
73 else who.textContent = h.actor_name || handleOf(h.actor_uri, h.actor_handle);
74 row.appendChild(who);
75 row.appendChild(el('span', 'when', when(h, h.published || h.created_at)));
76 card.appendChild(row);
77 var body = el('div', 'body g-note');
78 // body_html is the shared note-body partial, rendered server-side: the
79 // content with its emojis, the quote / link-preview card and the media.
80 // Falls back to the bare content for rows stored before that existed.
81 body.innerHTML = h.body_html || h.content || ''; // sanitized server-side on ingest
82 card.appendChild(body);
83 if (h.note_url) {
84 var a = el('a', 'g-link', T.open || 'open');
85 a.href = h.note_url; a.target = '_blank'; a.rel = 'noopener';
86 card.appendChild(a);
87 }
88 card.appendChild(helpState(h));
89 return card;
90 }
91
92 /** Hoe lang geleden, grof. Een oppik vervalt niet maar hoort wel te verouderen. */
93 function ago(ms) {
94 var u = Math.floor(ms / 3600000);
95 if (u < 1) return T.help_just_now || 'just now';
96 if (u < 24) return (T.help_hours || '{n}h ago').replace('{n}', u);
97 return (T.help_days || '{n}d ago').replace('{n}', Math.floor(u / 24));
98 }
99
100 /**
101 * De gedeelde staat van een hulpvraag (shaer-lgo): wie er al op af is, of het
102 * is afgesloten, en de twee knoppen.
103 *
104 * De faalstand hier is NIET veilig: 'iedereen denkt dat het geregeld is' is
105 * gevaarlijker dan geen markering. Dus bij twijfel toont dit OPEN, en een oude
106 * oppik verkleurt in plaats van te verdwijnen -- anders ziet een hulpvraag er
107 * onaangeroerd uit terwijl er iemand mee bezig is.
108 */
109 function helpState(h) {
110 var st = h.state || { open: true, pickedUpBy: [], handled: null, ageMs: null };
111 var box = el('div', 'g-help-state');
112
113 // Niet meer jouw ward: geen knoppen, want de route zou ze weigeren. En geen
114 // "afgehandeld", want dat zou een claim zijn over een kind waar je niets meer
115 // over te zeggen hebt.
116 if (st.formerWard) {
117 box.appendChild(el('div', 'g-help-former', T.help_former_ward || ''));
118 return box;
119 }
120
121 if (st.handled) {
122 var wie = st.handled.handle || handleOf(st.handled.uri);
123 box.appendChild(el('div', 'g-help-done', (T.help_handled_by || 'Handled by {who}').replace('{who}', wie)));
124 // Geen terugdraaiknop: leeft de vraag nog, dan wordt hij opnieuw gesteld.
125 box.appendChild(el('p', 'small g-empty', T.help_handled_note || ''));
126 return box;
127 }
128
129 (st.pickedUpBy || []).forEach(function (p) {
130 var line = (T.help_picked_by || '{who} is looking into this').replace('{who}', p.handle || handleOf(p.uri));
131 box.appendChild(el('div', 'g-help-pick', line));
132 });
133 // Oud, maar niet weg. Dit is het verschil tussen 'er is iemand mee bezig' en
134 // 'er was ooit iemand mee bezig'.
135 if (st.ageMs != null && st.ageMs > 3600000) {
136 box.appendChild(el('div', 'g-help-age small', ago(st.ageMs)));
137 }
138
139 var row = el('div', 'row');
140 var mij = (st.pickedUpBy || []).some(function (p) { return p.uri === S.me; });
141 if (!mij) {
142 var pick = el('button', 'small', T.help_pick || 'I am on it');
143 pick.addEventListener('click', function () { markHelp(h, 'pickup', pick); });
144 row.appendChild(pick);
145 }
146 var done = el('button', 'quiet small', T.help_close || 'Mark handled');
147 // Een stevige bevestiging, en nooit een window.confirm: dat verstopt een
148 // uitleg achter een OK die mensen wegklikken. Zelfde lijn als het loslaten
149 // van een ward.
150 done.addEventListener('click', function () {
151 done.hidden = true;
152 var ask = el('div', 'g-confirm');
153 ask.appendChild(el('p', 'small', T.help_close_ask || ''));
154 var ja = el('button', 'small', T.help_close_yes || 'Yes, handled');
155 ja.addEventListener('click', function () { markHelp(h, 'handled', ja); });
156 var nee = el('button', 'quiet small', T.release_no || 'No');
157 nee.addEventListener('click', function () { ask.remove(); done.hidden = false; });
158 ask.appendChild(ja); ask.appendChild(nee);
159 box.appendChild(ask);
160 });
161 row.appendChild(done);
162 box.appendChild(row);
163 return box;
164 }
165
166 function markHelp(h, kind, btn) {
167 if (btn) btn.disabled = true;
168 fetch('/guardian/api/help/' + kind, {
169 method: 'POST', headers: { 'Content-Type': 'application/json' },
170 body: JSON.stringify({ note: h.object_uri, ward: h.actor_uri, site: S.site }),
171 }).then(refresh).catch(function () { if (btn) btn.disabled = false; });
172 }
173
174 function renderHelp() {
175 var list = document.getElementById('help-list');
176 list.textContent = '';
177 var alle = S.help || [];
178 // Afgehandeld hoort niet meer in de lijst die om aandacht vraagt, en niet
179 // meer in de teller: anders blijft het cijfer alarm slaan voor iets dat af
180 // is, en neemt een gesloten vraag de ruimte in van een open vraag.
181 var open = alle.filter(helpOpen);
182 var klaar = alle.filter(function (h) { return !helpOpen(h); });
183
184 open.slice(0, HELP_TOP).forEach(function (h) { list.appendChild(helpCard(h)); });
185 if (open.length > HELP_TOP) {
186 list.appendChild(el('p', 'g-sec-sub', '+ ' + (open.length - HELP_TOP) + ' — ' + (T.panel_help || '')));
187 }
188
189 // WEG is niet hetzelfde als AF. Een afgehandelde hulpvraag blijft bestaan --
190 // er wordt in dit systeem niets herschreven -- maar hij hoort achter een
191 // klik, niet voor je neus. De volledige geschiedenis per kind staat sowieso
192 // in het paneel van dat kind.
193 if (klaar.length) {
194 var doos = el('div', 'g-help-archive');
195 doos.hidden = true;
196 klaar.forEach(function (h) { doos.appendChild(helpCard(h)); });
197 var knop = el('button', 'quiet small', (T.help_archive || '{n} handled').replace('{n}', klaar.length));
198 knop.addEventListener('click', function () {
199 doos.hidden = !doos.hidden;
200 knop.textContent = doos.hidden
201 ? (T.help_archive || '{n} handled').replace('{n}', klaar.length)
202 : (T.help_archive_hide || 'hide');
203 });
204 list.appendChild(knop);
205 list.appendChild(doos);
206 }
207
208 var badge = document.getElementById('help-count');
209 badge.textContent = open.length; badge.hidden = open.length === 0;
210 // "Geen hulpverzoeken. Mooi zo." mag ook staan als er wel een archief is:
211 // er wacht dan immers niets.
212 show('help-empty', open.length === 0);
213 }
214
215 // ── 3. Offers I am a party to (sent, or a co-guardianship to co-approve) ─
216 function answer(offerId, decision, btn) {
217 if (btn) btn.disabled = true;
218 fetch('/guardian/offer', {
219 method: 'POST', headers: { 'Content-Type': 'application/json' },
220 body: JSON.stringify({ offer: offerId, answer: decision, site: S.site }),
221 }).then(refresh);
222 }
223 function offerCard(o) {
224 var card = el('div', 'g-card');
225 var row = el('div', 'row');
226 var subject = o['shaer:iAmCandidate']
227 ? handleOf(o['shaer:ward'], o['shaer:wardHandle']) // my sent offer: about the ward
228 : handleOf(o['shaer:candidate'], o['shaer:candidateHandle']); // co-guard: who wants in
229 row.appendChild(el('span', 'who grow', subject));
230 if (o['shaer:iAmCandidate']) {
231 // My own offer, waiting for the others to accept.
232 row.appendChild(el('span', 'tag wait', T.pending));
233 var rt = el('button', 'quiet small', T.retract);
234 rt.addEventListener('click', function () { answer(o.id, 'reject', rt); });
235 row.appendChild(rt);
236 } else if (o['shaer:needsMyAccept']) {
237 // A co-guardianship offer for a ward I already guard: my call.
238 row.appendChild(el('span', 'tag co', T.coguard));
239 var ac = el('button', 'small', T.accept);
240 ac.addEventListener('click', function () { answer(o.id, 'accept', ac); });
241 var rj = el('button', 'quiet small', T.reject);
242 rj.addEventListener('click', function () { answer(o.id, 'reject', rj); });
243 row.appendChild(ac); row.appendChild(rj);
244 } else {
245 row.appendChild(el('span', 'tag wait', T.awaiting_others));
246 }
247 card.appendChild(row);
248 return card;
249 }
250 /** A running lapse (FEP-633c 3.6.3): the available co-guardians deciding
251 * to release a dormant one. Votes ride the same Accept/Reject wire as the
252 * offers; buttons appear only for set members (the ward watches, it does
253 * not vote). */
254 function lapseCard(l) {
255 var card = el('div', 'g-card lapse');
256 card.appendChild(el('div', 'who', (T.lapse_line || '{who} has stopped answering as a guardian of {ward}.')
257 .replace('{who}', handleOf(l.object.object)).replace('{ward}', handleOf(l.object['shaer:ward']))));
258 card.appendChild(el('div', 'g-avlabel', (T.lapse_tally || '{n} of {need} agreed; closes {date}.')
259 .replace('{n}', l['shaer:accepts']).replace('{need}', l['shaer:threshold'])
260 .replace('{date}', new Date(l['shaer:closesAt']).toLocaleDateString())));
261 var row = el('div', 'row');
262 var inSet = (l['shaer:set'] || []).indexOf(S.me) >= 0;
263 if (inSet && !l['shaer:myVote']) {
264 var yes = el('button', 'small', T.lapse_agree || 'Agree');
265 yes.addEventListener('click', function () { answer(l.id, 'accept', yes); });
266 var no = el('button', 'quiet small', T.lapse_disagree || 'Disagree');
267 no.addEventListener('click', function () { answer(l.id, 'reject', no); });
268 row.appendChild(yes); row.appendChild(no);
269 } else if (inSet) {
270 row.appendChild(el('span', 'g-avlabel', T.voted || 'You voted'));
271 }
272 card.appendChild(row);
273 card.appendChild(el('p', 'g-empty small', T.lapse_note || ''));
274 return card;
275 }
276
277 /** A gated-setting proposal (FEP-633c 5.6) a fellow guardian opened on a
278 * ward we share, forwarded here by the ward's server. Answering is the
279 * whole point: without a second voice the threshold is never met and the
280 * proposal quietly expires. */
281 function gatedCard(g) {
282 var card = el('div', 'g-card gated');
283 var line = g.value ? (T.gated_line_on || '') : (T.gated_line_off || '');
284 card.appendChild(el('div', 'who', line
285 .replace('{who}', handleOf(g.proposer || ''))
286 .replace('{ward}', handleOf(g.ward))));
287 var row = el('div', 'row');
288 var yes = el('button', 'small', T.gated_agree || 'Agree');
289 var no = el('button', 'quiet small', T.gated_disagree || 'Disagree');
290 function answerGated(decision, btn) {
291 btn.disabled = true;
292 fetch('/guardian/api/gated/' + encodeURIComponent(g.id), {
293 method: 'POST', headers: { 'Content-Type': 'application/json' },
294 body: JSON.stringify({ answer: decision, site: S.site }),
295 }).then(refresh).catch(function () { btn.disabled = false; });
296 }
297 yes.addEventListener('click', function () { answerGated('accept', yes); });
298 no.addEventListener('click', function () { answerGated('reject', no); });
299 row.appendChild(yes); row.appendChild(no);
300 card.appendChild(row);
301 return card;
302 }
303
304 function renderPending() {
305 var list = document.getElementById('pending-list');
306 list.textContent = '';
307 var offers = S.offers || [];
308 var gated = S.gatedReviews || [];
309 // The offers state carries the adoption offers; the lapse proposals ride
310 // separately so a lapse never renders as an adoption.
311 var lapses = (S.lapses || []).filter(function (l) { return l['shaer:outcome'] === 'open'; });
312 offers.forEach(function (o) {
313 if (o.object && o.object.type === 'shaer:Lapse') return; // rendered below
314 list.appendChild(offerCard(o));
315 });
316 lapses.forEach(function (l) { list.appendChild(lapseCard(l)); });
317 gated.forEach(function (g) { list.appendChild(gatedCard(g)); });
318 show('pending-section', offers.length > 0 || lapses.length > 0 || gated.length > 0);
319 }
320
321 // ── 4. Accepted wards: one panel per child ─────────────────────────────
322 // A guardian thinks per child, not per function, so everything about one
323 // child sits behind that child's row: the gated settings, the follow requests
324 // waiting on them, their calls for help, their recent posts. The row itself
325 // carries counts, so nothing that needs an answer hides inside a closed
326 // panel.
327 var openPanels = {}; // ward uri -> open, so a refresh does not close it
328 var openGates = {}; // idem voor de poortenlijst: uitklappen mag niet elke 45s dichtvallen
329 // The follow requests and the wards' posts arrive from their own endpoints
330 // and are grouped into the panels by ward, so they are cached here rather
331 // than rendered into a section of their own.
332 var FEED = [], FOLLOWS = [];
333
334 function sectionInto(panel, title, items, empty, build) {
335 var h = el('div', 'g-panel-sec');
336 h.appendChild(el('h3', null, title));
337 if (!items.length) h.appendChild(el('p', 'g-empty small', empty));
338 else items.forEach(function (it) { h.appendChild(build(it)); });
339 panel.appendChild(h);
340 return h;
341 }
342
343 function gateButton(w, feature, current, proposeLabel, onLabel, offLabel) {
344 var known = current === true || current === false;
345 var btn = el('button', 'quiet small', (known ? (current ? onLabel : offLabel) : proposeLabel) || feature);
346 btn.addEventListener('click', function () {
347 btn.disabled = true;
348 fetch('/guardian/wards/embeds', {
349 method: 'POST', headers: { 'Content-Type': 'application/json' },
350 body: JSON.stringify({ uri: w.other_uri, feature: feature, allow: known ? !current : true }),
351 }).then(function (r) { return r.json(); })
352 .then(function (j) {
353 if (j && j.state === 'open') {
354 btn.textContent = (T.embeds_waiting || 'waiting for the other guardians');
355 btn.disabled = true;
356 return;
357 }
358 refresh();
359 })
360 .catch(function () { btn.disabled = false; });
361 });
362 return btn;
363 }
364
365 function embedsButton(w) {
366 // Gated feature: external (non-fediverse) embeds. Off by default for a
367 // ward; only a guardian can open it, and the gate is enforced server-side
368 // when the feed is built, so this button is the only thing that moves it.
369 // Shown for EVERY ward, including one on another server. There the value is
370 // unknown (it lives on the ward's server), but proposing is exactly as
371 // possible: the proposal travels, the ward's server tallies the guardians
372 // and enforces. A guardian next door must not have more say than one far
373 // away.
374 var known = w.embeds === true || w.embeds === false;
375 var emb = el('button', 'quiet small',
376 (known ? (w.embeds ? T.embeds_on : T.embeds_off) : T.embeds_propose) || 'Link previews');
377 emb.addEventListener('click', function () {
378 emb.disabled = true;
379 fetch('/guardian/wards/embeds', {
380 method: 'POST', headers: { 'Content-Type': 'application/json' },
381 body: JSON.stringify({ uri: w.other_uri, allow: known ? !w.embeds : true }),
382 }).then(function (r) { return r.json(); })
383 .then(function (j) {
384 // Not settled yet: the other guardians still have to answer.
385 if (j && j.state === 'open') {
386 emb.textContent = (T.embeds_waiting || 'waiting for the other guardians');
387 emb.disabled = true;
388 return;
389 }
390 refresh();
391 })
392 .catch(function () { emb.disabled = false; });
393 });
394 return emb;
395 }
396
397 /**
398 * The second step of releasing a ward: what it does, then yes or no.
399 *
400 * The warning is assembled from what the server found, not from a fixed
401 * sentence, because releasing means two different things (FEP-633c): stepping
402 * down while other guardians remain (§3.3), or being the last one, which is
403 * emancipation and explicitly not one guardian's call (§3.4). And as long as
404 * the Undo does not federate, the ward's server keeps listing you either way
405 * — a guardian has to know that before pressing, not after.
406 */
407 function releaseStep(w, check, host, relBtn) {
408 var uri = w.other_uri;
409 var who = handleOf(uri, w.other_handle);
410 var box = el('div', 'g-warn');
411 box.appendChild(el('strong', null, (T.release_title || 'Release {who}?').replace('{who}', who)));
412 box.appendChild(el('p', null, T.release_effect || ''));
413 if (check.last === true) box.appendChild(el('p', 'grave', T.release_last || ''));
414 else if (check.last === false) box.appendChild(el('p', null, T.release_step_down || ''));
415 else box.appendChild(el('p', 'grave', T.release_unknown || ''));
416 box.appendChild(el('p', null, T.release_local || ''));
417
418 var row = el('div', 'row');
419 var no = el('button', 'small', T.release_no || 'No');
420 no.addEventListener('click', function () {
421 host.removeChild(box);
422 relBtn.hidden = false; relBtn.disabled = false;
423 });
424 // No first: the way out should be the easy one to hit.
425 row.appendChild(no);
426 // Being the last guardian is not a warning but a wall: the server refuses
427 // it (§3.4), so offering a yes here would only produce an error. The text
428 // above already says what has to happen instead.
429 if (check.last !== true) {
430 var yes = el('button', 'danger small', T.release_yes || 'Yes');
431 yes.addEventListener('click', function () {
432 yes.disabled = true;
433 remove(uri, yes, function (err) {
434 // The guardian set can change between the check and the click.
435 yes.disabled = false;
436 box.appendChild(el('p', 'grave', err === 'would_emancipate' ? (T.release_last || '') : (T.failed || '')));
437 if (err === 'would_emancipate') yes.remove();
438 });
439 });
440 row.appendChild(yes);
441 }
442 box.appendChild(row);
443 return box;
444 }
445
446 /** The availability dot (FEP-633c 3.6): buddy-list language on the
447 * responsibility axis. Green available, yellow declared away with an end,
448 * grey observed dormant (one answer restores). */
449 function availLabel(g) {
450 if (g.availability === 'away') {
451 var date = g.awayUntil ? new Date(g.awayUntil).toLocaleDateString() : '?';
452 return (T.avail_away || 'Unavailable till {date}').replace('{date}', date);
453 }
454 if (g.availability === 'dormant') return T.avail_dormant || 'Offline';
455 return T.avail_available || 'Available';
456 }
457 function availRow(g, wardUri) {
458 var row = el('div', 'row g-guard');
459 var dot = el('span', 'g-avdot ' + (g.availability === 'away' ? 'is-away' : g.availability === 'dormant' ? 'is-dormant' : 'is-active'));
460 row.appendChild(dot);
461 row.appendChild(el('span', 'who grow', handleOf(g.uri, g.handle)));
462 row.appendChild(el('span', 'g-avlabel', availLabel(g)));
463 // A dormant fellow guardian without a running lapse: the deliberate,
464 // rare next step (3.6.3). Never shown for anyone still answering.
465 if (g.availability === 'dormant' && !g.lapse && g.uri !== S.me) {
466 var btn = el('button', 'quiet small', T.lapse_propose || 'Propose release');
467 btn.addEventListener('click', function () {
468 btn.disabled = true;
469 fetch('/guardian/api/lapse', {
470 method: 'POST', headers: { 'Content-Type': 'application/json' },
471 body: JSON.stringify({ ward: wardUri, target: g.uri, site: S.site }),
472 }).then(refresh).catch(function () { btn.disabled = false; });
473 });
474 row.appendChild(btn);
475 }
476 return row;
477 }
478
479 /** Het woord dat we voor een gate gebruiken; onbekend valt terug op de naam. */
480 function gateLabel(feature) {
481 return T['gate_' + feature.replace('shaer:', '')] || feature.replace('shaer:', '');
482 }
483
484 /**
485 * Een rij in het gate-paneel. Toont WAT er geldt, van welk SOORT het is, welke
486 * drempel er hoort en wat er loopt -- en pas daarna een knop, als er iets te
487 * verzetten valt.
488 *
489 * Het soort staat erbij omdat de gates niet hetzelfde werken: een stand is aan
490 * of uit, volgverzoeken zijn een stroom beslissingen, en een overdracht is
491 * onomkeerbaar zodra het kind hem gebruikt. Vier rijen met dezelfde schakelaar
492 * zouden dat verschil wegpoetsen.
493 */
494 function gateRow(w, g) {
495 // Drie standen, niet twee. "Uit" en "voor zover wij weten uit" zijn niet
496 // hetzelfde: dat tweede betekent dat niemand er ooit over besloot, en dat
497 // hoort niet te lezen als een genomen besluit.
498 var stand = g.value === null || g.value === undefined
499 ? (T.gate_unknown || 'unknown')
500 : (g.value ? (T.prop_on || 'on') : (T.prop_off || 'off'));
501 if (!g.decided && g.value === false) stand = T.gate_default_off || stand;
502 var row = el('div', 'g-gate');
503 var head = el('div', 'g-gate-head');
504 head.appendChild(el('span', 'g-gate-name', gateLabel(g.feature)));
505 row.appendChild(head);
506
507 // De chip draagt de STAND en wisselt dus mee. Hij stond hier eerst op het
508 // SOORT -- in het Nederlands "stand" -- vlak boven de werkelijke stand, dus
509 // twee dingen die hetzelfde heetten waarvan er een nooit veranderde. Het
510 // veranderlijke hoort bovenaan; het beschrijvende gaat naar de regel eronder.
511 // Wat er nog niet is leest niet als een dichte poort maar als een lege plek.
512 // 'Uit' zou onwaar zijn: plaatjes werken vandaag gewoon.
513 if (!g.available) stand = T.gate_unavailable || 'not available yet';
514 var soort = g.available === false ? 'planned'
515 : (g.value === true ? 'on' : (g.decided ? 'off' : 'undecided'));
516 head.appendChild(el('span', 'g-gate-chip g-gate-chip-' + soort, stand));
517
518 var meta = el('div', 'g-gate-meta small');
519 meta.appendChild(el('span', 'g-gate-kind', T['gate_kind_' + g.kind] || g.kind));
520 if (!g.available) meta.appendChild(el('span', 'g-dim', T.gate_planned_note || ''));
521 if (g.threshold && g.available) {
522 meta.appendChild(el('span', null, (T.gate_threshold || '{need} of {of} guardians')
523 .replace('{need}', g.threshold.need).replace('{of}', g.threshold.of)));
524 } else if (g.available) {
525 // Geen drempel verzinnen die we niet kennen.
526 meta.appendChild(el('span', 'g-dim', T.gate_threshold_unknown || ''));
527 }
528 if (!g.reversible) meta.appendChild(el('span', 'g-gate-warn', T.gate_irreversible || ''));
529 if (g.waiting) {
530 meta.appendChild(el('span', 'g-gate-wait', (T.gate_waiting || '{n} waiting').replace('{n}', g.waiting)));
531 }
532 row.appendChild(meta);
533
534 if (g.proposal) {
535 row.appendChild(el('p', 'small g-prop g-prop-' + g.proposal.status,
536 (T.prop_line || 'Proposal {what} {value}: {status}')
537 .replace('{what}', gateLabel(g.feature))
538 .replace('{value}', g.proposal.value ? (T.prop_on || 'on') : (T.prop_off || 'off'))
539 .replace('{status}', T['prop_st_' + g.proposal.status] || g.proposal.status)));
540 }
541 if (g.blockedBy) {
542 row.appendChild(el('p', 'small g-empty', (T.gate_blocked || 'needs {what} first')
543 .replace('{what}', gateLabel(g.blockedBy))));
544 }
545 if (g.adjustable) {
546 // De knop zegt wat er GEBEURT, niet wat er staat -- de stand staat hierboven
547 // al. Staat de poort open, dan is het enige wat je kunt voorstellen hem
548 // dichtzetten, en dat hoort op de knop te staan voordat je klikt.
549 //
550 // De richting volgt de stand die we uit de besluiten kennen, dus ook voor
551 // een ward elders kun je nu DICHTZETTEN voorstellen. Dat was de veilige
552 // richting die eerder ontbrak toen de stand altijd onbekend was.
553 var openzetten = T.gate_propose_open || T.gate_propose || 'Propose';
554 var dichtzetten = T.gate_propose_close || T.gate_propose || 'Propose';
555 row.appendChild(gateButton(w, g.feature, g.value, openzetten, dichtzetten, openzetten));
556 }
557 return row;
558 }
559
560 function wardPanel(w) {
561 var uri = w.other_uri;
562 var panel = el('div', 'g-panel');
563 panel.hidden = !openPanels[uri];
564
565 var set = el('div', 'g-panel-sec');
566 set.appendChild(el('h3', null, T.settings_title || 'Settings'));
567 // Een rij per gate, uit de catalogus van de server (shaer-ahy.1). Losse
568 // knoppen lieten een guardian zelf uitzoeken wat er allemaal geldt, en wat
569 // niet verstelbaar is stond nergens -- terwijl dat de helft van het antwoord
570 // is op "wat mag dit kind".
571 //
572 // INGEKLAPT met een samenvatting erboven. Met twaalf poorten duwt een open
573 // lijst alles wat eronder staat -- guardians, volgverzoeken, hulpvragen --
574 // van het scherm. Wat je meestal wilt weten is "staat er iets open en wacht
575 // er iets op mij", en dat past op een regel.
576 if ((w.gates || []).length) {
577 var lijst = el('div', 'g-gates');
578 lijst.hidden = !openGates[uri];
579 w.gates.forEach(function (g) { lijst.appendChild(gateRow(w, g)); });
580
581 var aan = w.gates.filter(function (g) { return g.available && g.value === true; }).length;
582 var wacht = w.gates.reduce(function (n, g) { return n + (g.waiting || 0) + (g.proposal && g.proposal.status === 'open' ? 1 : 0); }, 0);
583 var kop = el('button', 'quiet small g-gates-toggle');
584 var zetKop = function () {
585 kop.textContent = (lijst.hidden ? (T.gates_show || 'Show gates') : (T.gates_hide || 'Hide gates'))
586 + ' - ' + (T.gates_summary || '{n} gates - {on} on, {wait} waiting')
587 .replace('{n}', w.gates.length).replace('{on}', aan).replace('{wait}', wacht);
588 };
589 zetKop();
590 kop.addEventListener('click', function () {
591 lijst.hidden = !lijst.hidden;
592 openGates[uri] = !lijst.hidden; // blijft open over een verversing heen
593 zetKop();
594 });
595 set.appendChild(kop);
596 set.appendChild(lijst);
597 }
598 // Terugval voor een server die de catalogus nog niet stuurt: dan de twee
599 // knoppen zoals ze waren, zodat een oudere Klonkt niet met een leeg vak zit.
600 if (!(w.gates || []).length) {
601 var setRow = el('div', 'row');
602 setRow.appendChild(gateButton(w, 'shaer:externalEmbeds', w.embeds, T.embeds_propose, T.embeds_on, T.embeds_off));
603 if (w.embeds !== false) {
604 setRow.appendChild(gateButton(w, 'shaer:externalPlayback', w.playback, T.play_propose, T.play_on, T.play_off));
605 }
606 set.appendChild(setRow);
607 (w.proposals || []).forEach(function (p) {
608 var what = p.feature === 'shaer:externalPlayback' ? (T.prop_play || 'playback') : (T.prop_embeds || 'link previews');
609 set.appendChild(el('p', 'small g-prop g-prop-' + p.status, (T.prop_line || 'Proposal {what} {value}: {status}')
610 .replace('{what}', what).replace('{value}', p.value ? (T.prop_on || 'on') : (T.prop_off || 'off'))
611 .replace('{status}', T['prop_st_' + p.status] || p.status)));
612 });
613 }
614 panel.appendChild(set);
615
616 // The fellow guardians of this child, with availability (3.6). For a
617 // ward on another server the states live there, and saying so honestly
618 // beats guessing.
619 var gsec = el('div', 'g-panel-sec');
620 gsec.appendChild(el('h3', null, T.panel_guards || 'Guardians of this child'));
621 if (w.guardians && w.guardians.length) {
622 w.guardians.forEach(function (g) { gsec.appendChild(availRow(g, uri)); });
623 } else {
624 // A ward on another server: WHO guards it is public on its actor
625 // (shaer:guardians, 2.1), so list the seats; availability is the ward
626 // server's private ledger (3.6.1) and is not shown, only named.
627 var placeholder = el('p', 'g-empty small', '…');
628 gsec.appendChild(placeholder);
629 fetch('/guardian/wards/guardians?site=' + encodeURIComponent(S.site) + '&uri=' + encodeURIComponent(uri))
630 .then(function (r) { return r.json(); })
631 .then(function (j) {
632 if (!j || !j.guardians || !j.guardians.length) {
633 placeholder.textContent = T.panel_guards_remote || '';
634 return;
635 }
636 placeholder.remove();
637 j.guardians.forEach(function (g) {
638 var row = el('div', 'row g-guard');
639 row.appendChild(el('span', 'who grow', handleOf(g.uri, g.handle)));
640 gsec.appendChild(row);
641 });
642 gsec.appendChild(el('p', 'g-empty small', T.panel_guards_far || ''));
643 })
644 .catch(function () { placeholder.textContent = T.panel_guards_remote || ''; });
645 }
646 panel.appendChild(gsec);
647
648 sectionInto(panel, T.panel_follow || 'Follow requests',
649 FOLLOWS.filter(function (f) { return f.wardUri === uri; }),
650 T.panel_follow_empty || '', followCard);
651
652 // Alleen wat nog WACHT. De afgehandelde vragen staan onderaan het paneel als
653 // eigen sectie: dooreen gezet lees je niet meer of er iets van je gevraagd
654 // wordt, en dat is de enige vraag die deze sectie hoort te beantwoorden.
655 sectionInto(panel, T.panel_help || 'Calls for help',
656 (S.help || []).filter(function (h) { return h.actor_uri === uri && helpOpen(h); }),
657 T.panel_help_empty || '', helpCard);
658
659 sectionInto(panel, T.panel_posts || 'Recent posts',
660 FEED.filter(function (p) { return p.authorUri === uri; }),
661 T.panel_posts_empty || '', feedCard);
662
663 // De geschiedenis: apart, en helemaal onderaan. Wat af is hoort niet tussen
664 // wat nog wacht -- daar wordt "moet ik iets doen" onleesbaar van. Weg gaat
665 // hij niet: er wordt in dit systeem niets herschreven, er wordt toegevoegd.
666 // Geen lege sectie als er nog niets is afgehandeld; dat is geen informatie.
667 var gedaan = (S.help || []).filter(function (h) { return h.actor_uri === uri && !helpOpen(h); });
668 if (gedaan.length) {
669 sectionInto(panel, (T.panel_history || 'Handled ({n})').replace('{n}', gedaan.length),
670 gedaan, '', helpCard).classList.add('g-panel-history');
671 }
672
673 var act = el('div', 'g-panel-sec');
674 act.appendChild(el('h3', null, T.panel_actions || 'Actions'));
675 var actRow = el('div', 'row');
676 var wave = el('button', 'small', T.wave || '👋 Wave');
677 wave.addEventListener('click', function () { sendWave(uri, wave); });
678 actRow.appendChild(wave);
679 var rel = el('button', 'quiet small', T.release);
680 // Letting a child go is a decision, not a click. It opens a step that first
681 // asks the server what releasing this particular ward actually does, then
682 // says it plainly and asks yes or no. Never window.confirm: that hides a
683 // long explanation behind an OK button people press to make it go away.
684 rel.addEventListener('click', function () {
685 rel.disabled = true;
686 // site matters: with several of your own sites the server would otherwise
687 // check this ward against the wrong one and answer "not my ward".
688 fetch('/guardian/wards/release-check?site=' + encodeURIComponent(S.site) + '&uri=' + encodeURIComponent(uri))
689 .then(function (r) { return r.json(); })
690 .then(function (c) {
691 rel.hidden = true;
692 act.appendChild(releaseStep(w, c || {}, act, rel));
693 })
694 .catch(function () { rel.disabled = false; });
695 });
696 actRow.appendChild(rel);
697 act.appendChild(actRow);
698 // Bovenaan, niet onderaan: zwaaien en loslaten zijn de dingen die je DOET.
699 // De rest van het paneel is lezen -- instellingen, wie er nog meer op let,
700 // wat er binnenkwam. Wie het paneel opent om iets te doen hoorde eerst langs
701 // vijf secties te scrollen.
702 panel.insertBefore(act, panel.firstChild);
703 return panel;
704 }
705
706 function renderWards() {
707 var list = document.getElementById('wards-list');
708 list.textContent = '';
709 var wards = S.wards || [];
710 wards.forEach(function (w) {
711 var uri = w.other_uri;
712 var card = el('div', 'g-card ward');
713 var row = el('div', 'row');
714 row.appendChild(el('span', 'who grow', handleOf(uri, w.other_handle)));
715 // Counts on the row: whatever is waiting must be visible with the panel shut.
716 // Alleen wat nog WACHT, zoals het commentaar hierboven al zei: een
717 // afgehandelde vraag hoorde de boei niet te laten staan.
718 var nHelp = (S.help || []).filter(function (h) { return h.actor_uri === uri && helpOpen(h); }).length;
719 var nFollow = FOLLOWS.filter(function (f) { return f.wardUri === uri; }).length;
720 if (nHelp) row.appendChild(el('span', 'tag help', '🛟 ' + nHelp));
721 if (nFollow) row.appendChild(el('span', 'tag co', nFollow + ' ' + (nFollow === 1 ? (T.badge_follow_one || '') : (T.badge_follow || ''))));
722 row.appendChild(el('span', 'tag ok', T.active));
723 var toggle = el('button', 'quiet small', openPanels[uri] ? T.panel_close : T.panel_open);
724 row.appendChild(toggle);
725 card.appendChild(row);
726 var panel = wardPanel(w);
727 card.appendChild(panel);
728 toggle.addEventListener('click', function () {
729 openPanels[uri] = !openPanels[uri];
730 panel.hidden = !openPanels[uri];
731 toggle.textContent = openPanels[uri] ? T.panel_close : T.panel_open;
732 });
733 list.appendChild(card);
734 });
735 show('wards-empty', wards.length === 0);
736 // Step away (3.6.1) only means something with wards to tell.
737 show('away-section', wards.length > 0);
738 }
739
740 // ── 4b. Step away (FEP-633c 3.6.1) ─────────────────────────────────────
741 function declareAway(days, btn) {
742 btn.disabled = true;
743 fetch('/guardian/api/away', {
744 method: 'POST', headers: { 'Content-Type': 'application/json' },
745 body: JSON.stringify({ days: days, site: S.site }),
746 }).then(function (r) { return r.json(); })
747 .then(function (j) {
748 btn.disabled = false;
749 var msg = document.getElementById('away-msg');
750 msg.hidden = false;
751 if (j && j.ok) {
752 msg.className = 'g-msg';
753 msg.textContent = (T.away_done || 'Your wards know you are unavailable until {date}.')
754 .replace('{date}', new Date(j.until).toLocaleDateString());
755 } else {
756 msg.className = 'g-msg err';
757 msg.textContent = (j && j.error) || (T.failed || 'failed');
758 }
759 })
760 .catch(function () { btn.disabled = false; });
761 }
762 var awayWeek = document.getElementById('away-week');
763 var awayMonth = document.getElementById('away-month');
764 if (awayWeek) awayWeek.addEventListener('click', function () { declareAway(7, awayWeek); });
765 if (awayMonth) awayMonth.addEventListener('click', function () { declareAway(30, awayMonth); });
766
767 function sendWave(uri, btn) {
768 btn.disabled = true;
769 fetch('/guardian/api/wave', {
770 method: 'POST', headers: { 'Content-Type': 'application/json' },
771 body: JSON.stringify({ ward: uri, site: S.site }),
772 }).then(function (r) { return r.json(); })
773 .then(function (j) { btn.disabled = false; btn.textContent = (j && j.ok) ? (T.waved || '👋 sent') : (T.wave || '👋 Wave'); })
774 .catch(function () { btn.disabled = false; });
775 }
776
777 function remove(uri, btn, onError) {
778 btn.disabled = true;
779 fetch('/guardian/wards/remove', {
780 method: 'POST', headers: { 'Content-Type': 'application/json' },
781 body: JSON.stringify({ uri: uri, site: S.site }),
782 }).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); })
783 .then(function (res) {
784 // The server can refuse: emptying shaer:guardians is emancipation and
785 // not one guardian's call (§3.4). Say so instead of silently redrawing.
786 if (!res.ok) { if (onError) onError(res.j && res.j.error); return; }
787 refresh();
788 })
789 .catch(function () { if (onError) onError('network'); else btn.disabled = false; });
790 }
791
792 function renderAll() { renderHelp(); renderPending(); renderWards(); }
793
794 // ── 0. Wards' corner: read-only feed of your wards' posts ───────────────
795 // Lives inside each child's panel now, so the fetches only fill a cache and
796 // ask the ward list to redraw. A guardian watches, it does not publish.
797 function feedCard(p) {
798 var card = el('div', 'g-card feed');
799 var head = el('div', 'row');
800 head.appendChild(el('span', 'who grow', p.author));
801 if (p.published) head.appendChild(el('span', 'g-when', when(p, p.published)));
802 card.appendChild(head);
803 var body = el('div', 'feed-body');
804 // body_html is de gedeelde note-body-partial, serverside gerenderd: opmaak,
805 // media, quote-kaart en embed, precies als in de Krant en in Berichten.
806 // Valt terug op de kale content voor een client uit de cache.
807 var html = p.body_html || p.content || '';
808 if (p.cw) {
809 // De content warning blijft van de PWA zelf: note-body versluiert alleen
810 // bij nsfw, en een ward-post met alleen een cw hoort hier dicht te staan.
811 var d = document.createElement('details');
812 var sum = document.createElement('summary'); sum.textContent = p.cw; d.appendChild(sum);
813 var inner = el('div'); inner.innerHTML = html; d.appendChild(inner);
814 body.appendChild(d);
815 } else {
816 body.innerHTML = html; // server-sanitized HTML (same as Berichten)
817 }
818 card.appendChild(body);
819 return card;
820 }
821
822 function loadFeed() {
823 return fetch('/guardian/api/feed?site=' + encodeURIComponent(S.site))
824 .then(function (r) { return r.json(); })
825 .then(function (f) { if (f && !f.error) { FEED = f.items || []; renderWards(); } })
826 .catch(function () { /* panels just show "nothing yet" */ });
827 }
828
829 // ── 0b. Follow requests on your wards (§5.3) ────────────────────────────
830 function answerFollow(id, decision, btn) {
831 if (btn) btn.disabled = true;
832 fetch('/guardian/api/follow/' + encodeURIComponent(id), {
833 method: 'POST', headers: { 'Content-Type': 'application/json' },
834 body: JSON.stringify({ decision: decision, site: S.site }),
835 }).then(loadFollowReqs);
836 }
837 function followCard(f) {
838 var card = el('div', 'g-card');
839 var row = el('div', 'row');
840 // Inside the child's own panel the ward name is a given, so only the
841 // person asking is named here.
842 row.appendChild(el('span', 'who grow', f.follower));
843 var ok = el('button', 'small', T.accept || 'Accept');
844 ok.addEventListener('click', function () { answerFollow(f.id, 'approve', ok); });
845 var no = el('button', 'quiet small', T.reject || 'Deny');
846 no.addEventListener('click', function () { answerFollow(f.id, 'reject', no); });
847 row.appendChild(ok); row.appendChild(no);
848 card.appendChild(row);
849 return card;
850 }
851 function loadFollowReqs() {
852 return fetch('/guardian/api/follow-requests?site=' + encodeURIComponent(S.site))
853 .then(function (r) { return r.json(); })
854 .then(function (f) { if (f && !f.error) { FOLLOWS = f.items || []; renderWards(); } })
855 .catch(function () { /* panels just show "none waiting" */ });
856 }
857
858 function refresh() {
859 return fetch('/guardian/api/state?site=' + encodeURIComponent(S.site))
860 .then(function (r) { return r.json(); })
861 .then(function (s) { if (s && !s.error) { S = s; T = s.strings || T; renderAll(); } })
862 .then(loadFeed).then(loadFollowReqs);
863 }
864
865 // ── 2. Adopt ───────────────────────────────────────────────────────────
866 var form = document.getElementById('adopt-form');
867 var input = document.getElementById('adopt-handle');
868 var adoptBtn = document.getElementById('adopt-btn');
869 var msg = document.getElementById('adopt-msg');
870 function setMsg(text, isErr) { msg.hidden = false; msg.className = 'g-msg' + (isErr ? ' err' : ''); msg.textContent = text; }
871
872 form.addEventListener('submit', function (ev) {
873 ev.preventDefault();
874 var handle = input.value.trim();
875 if (!handle) return;
876 adoptBtn.disabled = true;
877 setMsg(T.sending || '…', false);
878 fetch('/guardian/adopt', {
879 method: 'POST', headers: { 'Content-Type': 'application/json' },
880 body: JSON.stringify({ handle: handle, site: S.site }),
881 }).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); })
882 .then(function (res) {
883 adoptBtn.disabled = false;
884 if (res.ok) {
885 input.value = '';
886 // Always refresh: the offer is recorded even if delivery is still
887 // in flight. Show it under "Verzonden aanvragen".
888 setMsg(res.j.delivered === false ? T.sent_retry : T.sent, false);
889 refresh();
890 } else {
891 setMsg((res.j.error === 'not_found' ? T.not_found : T.failed) , true);
892 }
893 })
894 .catch(function () { adoptBtn.disabled = false; setMsg(T.network, true); });
895 });
896
897 // ── Site picker ────────────────────────────────────────────────────────
898 var picker = document.getElementById('site-picker');
899 if (picker) picker.addEventListener('change', function () {
900 location.href = '/guardian?site=' + encodeURIComponent(picker.value);
901 });
902
903 // ── 5. Push ────────────────────────────────────────────────────────────
904 var toggle = document.getElementById('push-toggle');
905 var pmsg = document.getElementById('push-msg');
906 function pushState() {
907 if (!('serviceWorker' in navigator) || !('PushManager' in window)) { toggle.disabled = true; return; }
908 navigator.serviceWorker.register('/sw.js').catch(function () {});
909 navigator.serviceWorker.ready
910 .then(function (reg) { return reg.pushManager.getSubscription(); })
911 .then(function (sub) {
912 toggle.textContent = sub ? toggle.dataset.onLabel : toggle.dataset.offLabel;
913 toggle.dataset.subscribed = sub ? '1' : '';
914 toggle.classList.toggle('is-on', !!sub);
915 });
916 }
917 function urlB64(base64) {
918 var pad = '='.repeat((4 - (base64.length % 4)) % 4);
919 var b = (base64 + pad).replace(/-/g, '+').replace(/_/g, '/');
920 var raw = atob(b); var arr = new Uint8Array(raw.length);
921 for (var i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
922 return arr;
923 }
924 toggle.addEventListener('click', function () {
925 pmsg.hidden = true;
926 navigator.serviceWorker.ready.then(function (reg) {
927 if (toggle.dataset.subscribed) {
928 reg.pushManager.getSubscription().then(function (sub) {
929 if (!sub) return;
930 fetch('/push/unsubscribe', {
931 method: 'POST', headers: { 'Content-Type': 'application/json' },
932 body: JSON.stringify({ endpoint: sub.endpoint }),
933 }).then(function () { return sub.unsubscribe(); }).then(pushState);
934 });
935 return;
936 }
937 fetch('/push/vapid').then(function (r) { return r.json(); }).then(function (v) {
938 if (!v.publicKey) throw new Error('no key');
939 return reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlB64(v.publicKey) });
940 }).then(function (sub) {
941 return fetch('/push/subscribe', {
942 method: 'POST', headers: { 'Content-Type': 'application/json' },
943 body: JSON.stringify({
944 subscription: sub.toJSON(),
945 alerts: { help: 1, guardian: 1, dm: 1, follow: 0, reply: 0, like: 0, boost: 0 },
946 uaLabel: 'Guardian PWA',
947 }),
948 });
949 }).then(pushState).catch(function (e) {
950 pmsg.hidden = false; pmsg.className = 'g-msg err';
951 pmsg.textContent = (T.push_unavailable || 'Push unavailable') + ': ' + e.message;
952 });
953 });
954 });
955
956 renderAll(); pushState(); loadFeed(); loadFollowReqs();
957
958 // De push die de melding brengt is meteen het teken dat de staat veranderd is.
959 // Daarmee hoeft er geen tweede, open verbinding bij: hetzelfde kanaal doet het
960 // werk, en het werkt ook als de app dicht is.
961 if ('serviceWorker' in navigator) {
962 navigator.serviceWorker.addEventListener('message', function (e) {
963 if (e.data && e.data.klonkt === 'push') refresh();
964 });
965 }
966 // Het tikje blijft als vangnet -- niet elke verandering geeft een melding, en
967 // niet iedereen heeft meldingen aanstaan. Maar niet tikken terwijl niemand
968 // kijkt: dat waren verzoeken voor een tabblad op de achtergrond. Bij terugkomen
969 // meteen een keer, want dan is de kans op nieuws het grootst.
970 setInterval(function () { if (!document.hidden) refresh(); }, 45000);
971 document.addEventListener('visibilitychange', function () { if (!document.hidden) refresh(); });
972 } catch (e) {
973 fatal((e && e.message) || String(e));
974 }
975})();
Note: See TracBrowser for help on using the repository browser.