source: Klonkt/src/assets/js/guardian.js@ 23da947

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

Alle poorten zichtbaar, en de lijst ingeklapt (shaer-ahy.1, vervolg)

Barts wens: de negen geplande gates ook tonen, en de lijst compacter -- het liefst
ingeklapt met een uitklap.

DE NEGEN STAAN ERIN, MAAR NIET ALS DICHTE DEUR. Acht ervan bestaan nog niet:
featureColumn() kent de namen niet, dus een voorstel zou stranden op
unknown_feature. En ze als "uit" tonen zou ronduit onwaar zijn -- plaatjes werken
vandaag gewoon. Ze krijgen daarom een eigen stand, "nog niet beschikbaar", met een
gestippelde chip, geen drempel en geen knop. Een lege plek, geen gesloten poort.

Elke geplande rij draagt zijn bead, zodat het paneel meteen de weg wijst naar waar
die gate gebouwd wordt.

Het kind van de geplande gates is voorlopig en dat staat er ook bij: of
accountmigratie een stand is of een besluit per keer hoort bij het bouwen van
shaer-tge beslist te worden, niet hier. Independence is de enige die gezag
OVERDRAAGT en dus als enige onomkeerbaar (shaer-90v).

INGEKLAPT MET EEN SAMENVATTING. Met twaalf poorten duwt een open lijst alles wat
eronder staat -- guardians, volgverzoeken, hulpvragen -- van het scherm. De regel
erboven zegt wat je meestal wilt weten: hoeveel poorten er zijn, hoeveel er aan
staan en hoeveel er op je wachten. Uitklappen blijft over een verversing heen
staan, net als het wardpaneel zelf; anders valt hij elke 45 seconden dicht.

Negen labels in nl/en/de. Suite 611/611.

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