source: Klonkt/src/assets/js/guardian.js@ 1ff801d

main
Last change on this file since 1ff801d was 1ff801d, checked in by roboburr <roboburr@โ€ฆ>, 5 weeks ago

De hulpboei op de wardregel telt alleen wat nog wacht (shaer-lgo, vervolg)

Barts melding: na het afhandelen bleef er een ๐Ÿ›Ÿ 1 op de wardregel staan.

Dezelfde fout als de vorige, op een tweede plek. De teller op die regel telde
alle hulpvragen van dat kind, ook afgehandelde -- terwijl het commentaar er direct
boven al zei "whatever is WAITING must be visible with the panel shut". De
bedoeling klopte, de uitvoering niet.

DE ECHTE LES: "open" werd op twee plekken los beslist. Ik filterde de lijst en
dacht dat ik klaar was. Nu is er een helpOpen(h), en die is de enige plek waar
dat wordt uitgemaakt -- met in het commentaar waarom, zodat een derde plek er
langskomt in plaats van het opnieuw te bedenken.

Bij twijfel telt hij OPEN: alles wat geen expliciete afsluiting draagt wacht nog.
De omgekeerde fout -- iets als afgehandeld tonen dat het niet is -- is hier de
gevaarlijke.

Ook: in het paneel van een kind blijft de VOLLEDIGE geschiedenis staan (daar is
dat paneel voor), maar staat wat nog wacht bovenaan. Dat is waar je naar zoekt
als je het openslaat.

Suite 576/576, al raakt die dit niet: client-JS.

  • Property mode set to 100644
File size: 42.9 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 // The follow requests and the wards' posts arrive from their own endpoints
321 // and are grouped into the panels by ward, so they are cached here rather
322 // than rendered into a section of their own.
323 var FEED = [], FOLLOWS = [];
324
325 function sectionInto(panel, title, items, empty, build) {
326 var h = el('div', 'g-panel-sec');
327 h.appendChild(el('h3', null, title));
328 if (!items.length) h.appendChild(el('p', 'g-empty small', empty));
329 else items.forEach(function (it) { h.appendChild(build(it)); });
330 panel.appendChild(h);
331 return h;
332 }
333
334 function gateButton(w, feature, current, proposeLabel, onLabel, offLabel) {
335 var known = current === true || current === false;
336 var btn = el('button', 'quiet small', (known ? (current ? onLabel : offLabel) : proposeLabel) || feature);
337 btn.addEventListener('click', function () {
338 btn.disabled = true;
339 fetch('/guardian/wards/embeds', {
340 method: 'POST', headers: { 'Content-Type': 'application/json' },
341 body: JSON.stringify({ uri: w.other_uri, feature: feature, allow: known ? !current : true }),
342 }).then(function (r) { return r.json(); })
343 .then(function (j) {
344 if (j && j.state === 'open') {
345 btn.textContent = (T.embeds_waiting || 'waiting for the other guardians');
346 btn.disabled = true;
347 return;
348 }
349 refresh();
350 })
351 .catch(function () { btn.disabled = false; });
352 });
353 return btn;
354 }
355
356 function embedsButton(w) {
357 // Gated feature: external (non-fediverse) embeds. Off by default for a
358 // ward; only a guardian can open it, and the gate is enforced server-side
359 // when the feed is built, so this button is the only thing that moves it.
360 // Shown for EVERY ward, including one on another server. There the value is
361 // unknown (it lives on the ward's server), but proposing is exactly as
362 // possible: the proposal travels, the ward's server tallies the guardians
363 // and enforces. A guardian next door must not have more say than one far
364 // away.
365 var known = w.embeds === true || w.embeds === false;
366 var emb = el('button', 'quiet small',
367 (known ? (w.embeds ? T.embeds_on : T.embeds_off) : T.embeds_propose) || 'Link previews');
368 emb.addEventListener('click', function () {
369 emb.disabled = true;
370 fetch('/guardian/wards/embeds', {
371 method: 'POST', headers: { 'Content-Type': 'application/json' },
372 body: JSON.stringify({ uri: w.other_uri, allow: known ? !w.embeds : true }),
373 }).then(function (r) { return r.json(); })
374 .then(function (j) {
375 // Not settled yet: the other guardians still have to answer.
376 if (j && j.state === 'open') {
377 emb.textContent = (T.embeds_waiting || 'waiting for the other guardians');
378 emb.disabled = true;
379 return;
380 }
381 refresh();
382 })
383 .catch(function () { emb.disabled = false; });
384 });
385 return emb;
386 }
387
388 /**
389 * The second step of releasing a ward: what it does, then yes or no.
390 *
391 * The warning is assembled from what the server found, not from a fixed
392 * sentence, because releasing means two different things (FEP-633c): stepping
393 * down while other guardians remain (ยง3.3), or being the last one, which is
394 * emancipation and explicitly not one guardian's call (ยง3.4). And as long as
395 * the Undo does not federate, the ward's server keeps listing you either way
396 * โ€” a guardian has to know that before pressing, not after.
397 */
398 function releaseStep(w, check, host, relBtn) {
399 var uri = w.other_uri;
400 var who = handleOf(uri, w.other_handle);
401 var box = el('div', 'g-warn');
402 box.appendChild(el('strong', null, (T.release_title || 'Release {who}?').replace('{who}', who)));
403 box.appendChild(el('p', null, T.release_effect || ''));
404 if (check.last === true) box.appendChild(el('p', 'grave', T.release_last || ''));
405 else if (check.last === false) box.appendChild(el('p', null, T.release_step_down || ''));
406 else box.appendChild(el('p', 'grave', T.release_unknown || ''));
407 box.appendChild(el('p', null, T.release_local || ''));
408
409 var row = el('div', 'row');
410 var no = el('button', 'small', T.release_no || 'No');
411 no.addEventListener('click', function () {
412 host.removeChild(box);
413 relBtn.hidden = false; relBtn.disabled = false;
414 });
415 // No first: the way out should be the easy one to hit.
416 row.appendChild(no);
417 // Being the last guardian is not a warning but a wall: the server refuses
418 // it (ยง3.4), so offering a yes here would only produce an error. The text
419 // above already says what has to happen instead.
420 if (check.last !== true) {
421 var yes = el('button', 'danger small', T.release_yes || 'Yes');
422 yes.addEventListener('click', function () {
423 yes.disabled = true;
424 remove(uri, yes, function (err) {
425 // The guardian set can change between the check and the click.
426 yes.disabled = false;
427 box.appendChild(el('p', 'grave', err === 'would_emancipate' ? (T.release_last || '') : (T.failed || '')));
428 if (err === 'would_emancipate') yes.remove();
429 });
430 });
431 row.appendChild(yes);
432 }
433 box.appendChild(row);
434 return box;
435 }
436
437 /** The availability dot (FEP-633c 3.6): buddy-list language on the
438 * responsibility axis. Green available, yellow declared away with an end,
439 * grey observed dormant (one answer restores). */
440 function availLabel(g) {
441 if (g.availability === 'away') {
442 var date = g.awayUntil ? new Date(g.awayUntil).toLocaleDateString() : '?';
443 return (T.avail_away || 'Unavailable till {date}').replace('{date}', date);
444 }
445 if (g.availability === 'dormant') return T.avail_dormant || 'Offline';
446 return T.avail_available || 'Available';
447 }
448 function availRow(g, wardUri) {
449 var row = el('div', 'row g-guard');
450 var dot = el('span', 'g-avdot ' + (g.availability === 'away' ? 'is-away' : g.availability === 'dormant' ? 'is-dormant' : 'is-active'));
451 row.appendChild(dot);
452 row.appendChild(el('span', 'who grow', handleOf(g.uri, g.handle)));
453 row.appendChild(el('span', 'g-avlabel', availLabel(g)));
454 // A dormant fellow guardian without a running lapse: the deliberate,
455 // rare next step (3.6.3). Never shown for anyone still answering.
456 if (g.availability === 'dormant' && !g.lapse && g.uri !== S.me) {
457 var btn = el('button', 'quiet small', T.lapse_propose || 'Propose release');
458 btn.addEventListener('click', function () {
459 btn.disabled = true;
460 fetch('/guardian/api/lapse', {
461 method: 'POST', headers: { 'Content-Type': 'application/json' },
462 body: JSON.stringify({ ward: wardUri, target: g.uri, site: S.site }),
463 }).then(refresh).catch(function () { btn.disabled = false; });
464 });
465 row.appendChild(btn);
466 }
467 return row;
468 }
469
470 /** Het woord dat we voor een gate gebruiken; onbekend valt terug op de naam. */
471 function gateLabel(feature) {
472 return T['gate_' + feature.replace('shaer:', '')] || feature.replace('shaer:', '');
473 }
474
475 /**
476 * Een rij in het gate-paneel. Toont WAT er geldt, van welk SOORT het is, welke
477 * drempel er hoort en wat er loopt -- en pas daarna een knop, als er iets te
478 * verzetten valt.
479 *
480 * Het soort staat erbij omdat de gates niet hetzelfde werken: een stand is aan
481 * of uit, volgverzoeken zijn een stroom beslissingen, en een overdracht is
482 * onomkeerbaar zodra het kind hem gebruikt. Vier rijen met dezelfde schakelaar
483 * zouden dat verschil wegpoetsen.
484 */
485 function gateRow(w, g) {
486 var row = el('div', 'g-gate');
487 var head = el('div', 'g-gate-head');
488 head.appendChild(el('span', 'g-gate-name', gateLabel(g.feature)));
489 head.appendChild(el('span', 'g-gate-kind', T['gate_kind_' + g.kind] || g.kind));
490 row.appendChild(head);
491
492 // De stand. NULL is onbekend en dat is iets anders dan uit: bij een ward op
493 // een andere server wordt die kolom daar bijgehouden.
494 var stand = g.value === null || g.value === undefined
495 ? (T.gate_unknown || 'unknown')
496 : (g.value ? (T.prop_on || 'on') : (T.prop_off || 'off'));
497 var meta = el('div', 'g-gate-meta small');
498 meta.appendChild(el('span', null, stand));
499 if (g.threshold) {
500 meta.appendChild(el('span', null, (T.gate_threshold || '{need} of {of} guardians')
501 .replace('{need}', g.threshold.need).replace('{of}', g.threshold.of)));
502 } else {
503 // Geen drempel verzinnen die we niet kennen.
504 meta.appendChild(el('span', 'g-dim', T.gate_threshold_unknown || ''));
505 }
506 if (!g.reversible) meta.appendChild(el('span', 'g-gate-warn', T.gate_irreversible || ''));
507 if (g.waiting) {
508 meta.appendChild(el('span', 'g-gate-wait', (T.gate_waiting || '{n} waiting').replace('{n}', g.waiting)));
509 }
510 row.appendChild(meta);
511
512 if (g.proposal) {
513 row.appendChild(el('p', 'small g-prop g-prop-' + g.proposal.status,
514 (T.prop_line || 'Proposal {what} {value}: {status}')
515 .replace('{what}', gateLabel(g.feature))
516 .replace('{value}', g.proposal.value ? (T.prop_on || 'on') : (T.prop_off || 'off'))
517 .replace('{status}', T['prop_st_' + g.proposal.status] || g.proposal.status)));
518 }
519 if (g.blockedBy) {
520 row.appendChild(el('p', 'small g-empty', (T.gate_blocked || 'needs {what} first')
521 .replace('{what}', gateLabel(g.blockedBy))));
522 }
523 if (g.adjustable) {
524 row.appendChild(gateButton(w, g.feature, g.value, T.gate_propose || T.embeds_propose,
525 T.prop_on || 'on', T.prop_off || 'off'));
526 }
527 return row;
528 }
529
530 function wardPanel(w) {
531 var uri = w.other_uri;
532 var panel = el('div', 'g-panel');
533 panel.hidden = !openPanels[uri];
534
535 var set = el('div', 'g-panel-sec');
536 set.appendChild(el('h3', null, T.settings_title || 'Settings'));
537 // Een rij per gate, uit de catalogus van de server (shaer-ahy.1). Losse
538 // knoppen lieten een guardian zelf uitzoeken wat er allemaal geldt, en wat
539 // niet verstelbaar is stond nergens -- terwijl dat de helft van het antwoord
540 // is op "wat mag dit kind".
541 (w.gates || []).forEach(function (g) { set.appendChild(gateRow(w, g)); });
542 // Terugval voor een server die de catalogus nog niet stuurt: dan de twee
543 // knoppen zoals ze waren, zodat een oudere Klonkt niet met een leeg vak zit.
544 if (!(w.gates || []).length) {
545 var setRow = el('div', 'row');
546 setRow.appendChild(gateButton(w, 'shaer:externalEmbeds', w.embeds, T.embeds_propose, T.embeds_on, T.embeds_off));
547 if (w.embeds !== false) {
548 setRow.appendChild(gateButton(w, 'shaer:externalPlayback', w.playback, T.play_propose, T.play_on, T.play_off));
549 }
550 set.appendChild(setRow);
551 (w.proposals || []).forEach(function (p) {
552 var what = p.feature === 'shaer:externalPlayback' ? (T.prop_play || 'playback') : (T.prop_embeds || 'link previews');
553 set.appendChild(el('p', 'small g-prop g-prop-' + p.status, (T.prop_line || 'Proposal {what} {value}: {status}')
554 .replace('{what}', what).replace('{value}', p.value ? (T.prop_on || 'on') : (T.prop_off || 'off'))
555 .replace('{status}', T['prop_st_' + p.status] || p.status)));
556 });
557 }
558 panel.appendChild(set);
559
560 // The fellow guardians of this child, with availability (3.6). For a
561 // ward on another server the states live there, and saying so honestly
562 // beats guessing.
563 var gsec = el('div', 'g-panel-sec');
564 gsec.appendChild(el('h3', null, T.panel_guards || 'Guardians of this child'));
565 if (w.guardians && w.guardians.length) {
566 w.guardians.forEach(function (g) { gsec.appendChild(availRow(g, uri)); });
567 } else {
568 // A ward on another server: WHO guards it is public on its actor
569 // (shaer:guardians, 2.1), so list the seats; availability is the ward
570 // server's private ledger (3.6.1) and is not shown, only named.
571 var placeholder = el('p', 'g-empty small', 'โ€ฆ');
572 gsec.appendChild(placeholder);
573 fetch('/guardian/wards/guardians?site=' + encodeURIComponent(S.site) + '&uri=' + encodeURIComponent(uri))
574 .then(function (r) { return r.json(); })
575 .then(function (j) {
576 if (!j || !j.guardians || !j.guardians.length) {
577 placeholder.textContent = T.panel_guards_remote || '';
578 return;
579 }
580 placeholder.remove();
581 j.guardians.forEach(function (g) {
582 var row = el('div', 'row g-guard');
583 row.appendChild(el('span', 'who grow', handleOf(g.uri, g.handle)));
584 gsec.appendChild(row);
585 });
586 gsec.appendChild(el('p', 'g-empty small', T.panel_guards_far || ''));
587 })
588 .catch(function () { placeholder.textContent = T.panel_guards_remote || ''; });
589 }
590 panel.appendChild(gsec);
591
592 sectionInto(panel, T.panel_follow || 'Follow requests',
593 FOLLOWS.filter(function (f) { return f.wardUri === uri; }),
594 T.panel_follow_empty || '', followCard);
595
596 // Hier juist de VOLLEDIGE geschiedenis van dit kind -- dat is waar dit
597 // paneel voor is. Wel wat nog wacht bovenaan, want dat is waar je naar zoekt
598 // als je het opent.
599 sectionInto(panel, T.panel_help || 'Calls for help',
600 (S.help || []).filter(function (h) { return h.actor_uri === uri; })
601 .sort(function (a, b) { return (helpOpen(b) ? 1 : 0) - (helpOpen(a) ? 1 : 0); }),
602 T.panel_help_empty || '', helpCard);
603
604 sectionInto(panel, T.panel_posts || 'Recent posts',
605 FEED.filter(function (p) { return p.authorUri === uri; }),
606 T.panel_posts_empty || '', feedCard);
607
608 var act = el('div', 'g-panel-sec');
609 act.appendChild(el('h3', null, T.panel_actions || 'Actions'));
610 var actRow = el('div', 'row');
611 var wave = el('button', 'small', T.wave || '๐Ÿ‘‹ Wave');
612 wave.addEventListener('click', function () { sendWave(uri, wave); });
613 actRow.appendChild(wave);
614 var rel = el('button', 'quiet small', T.release);
615 // Letting a child go is a decision, not a click. It opens a step that first
616 // asks the server what releasing this particular ward actually does, then
617 // says it plainly and asks yes or no. Never window.confirm: that hides a
618 // long explanation behind an OK button people press to make it go away.
619 rel.addEventListener('click', function () {
620 rel.disabled = true;
621 // site matters: with several of your own sites the server would otherwise
622 // check this ward against the wrong one and answer "not my ward".
623 fetch('/guardian/wards/release-check?site=' + encodeURIComponent(S.site) + '&uri=' + encodeURIComponent(uri))
624 .then(function (r) { return r.json(); })
625 .then(function (c) {
626 rel.hidden = true;
627 act.appendChild(releaseStep(w, c || {}, act, rel));
628 })
629 .catch(function () { rel.disabled = false; });
630 });
631 actRow.appendChild(rel);
632 act.appendChild(actRow);
633 // Bovenaan, niet onderaan: zwaaien en loslaten zijn de dingen die je DOET.
634 // De rest van het paneel is lezen -- instellingen, wie er nog meer op let,
635 // wat er binnenkwam. Wie het paneel opent om iets te doen hoorde eerst langs
636 // vijf secties te scrollen.
637 panel.insertBefore(act, panel.firstChild);
638 return panel;
639 }
640
641 function renderWards() {
642 var list = document.getElementById('wards-list');
643 list.textContent = '';
644 var wards = S.wards || [];
645 wards.forEach(function (w) {
646 var uri = w.other_uri;
647 var card = el('div', 'g-card ward');
648 var row = el('div', 'row');
649 row.appendChild(el('span', 'who grow', handleOf(uri, w.other_handle)));
650 // Counts on the row: whatever is waiting must be visible with the panel shut.
651 // Alleen wat nog WACHT, zoals het commentaar hierboven al zei: een
652 // afgehandelde vraag hoorde de boei niet te laten staan.
653 var nHelp = (S.help || []).filter(function (h) { return h.actor_uri === uri && helpOpen(h); }).length;
654 var nFollow = FOLLOWS.filter(function (f) { return f.wardUri === uri; }).length;
655 if (nHelp) row.appendChild(el('span', 'tag help', '๐Ÿ›Ÿ ' + nHelp));
656 if (nFollow) row.appendChild(el('span', 'tag co', nFollow + ' ' + (nFollow === 1 ? (T.badge_follow_one || '') : (T.badge_follow || ''))));
657 row.appendChild(el('span', 'tag ok', T.active));
658 var toggle = el('button', 'quiet small', openPanels[uri] ? T.panel_close : T.panel_open);
659 row.appendChild(toggle);
660 card.appendChild(row);
661 var panel = wardPanel(w);
662 card.appendChild(panel);
663 toggle.addEventListener('click', function () {
664 openPanels[uri] = !openPanels[uri];
665 panel.hidden = !openPanels[uri];
666 toggle.textContent = openPanels[uri] ? T.panel_close : T.panel_open;
667 });
668 list.appendChild(card);
669 });
670 show('wards-empty', wards.length === 0);
671 // Step away (3.6.1) only means something with wards to tell.
672 show('away-section', wards.length > 0);
673 }
674
675 // โ”€โ”€ 4b. Step away (FEP-633c 3.6.1) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
676 function declareAway(days, btn) {
677 btn.disabled = true;
678 fetch('/guardian/api/away', {
679 method: 'POST', headers: { 'Content-Type': 'application/json' },
680 body: JSON.stringify({ days: days, site: S.site }),
681 }).then(function (r) { return r.json(); })
682 .then(function (j) {
683 btn.disabled = false;
684 var msg = document.getElementById('away-msg');
685 msg.hidden = false;
686 if (j && j.ok) {
687 msg.className = 'g-msg';
688 msg.textContent = (T.away_done || 'Your wards know you are unavailable until {date}.')
689 .replace('{date}', new Date(j.until).toLocaleDateString());
690 } else {
691 msg.className = 'g-msg err';
692 msg.textContent = (j && j.error) || (T.failed || 'failed');
693 }
694 })
695 .catch(function () { btn.disabled = false; });
696 }
697 var awayWeek = document.getElementById('away-week');
698 var awayMonth = document.getElementById('away-month');
699 if (awayWeek) awayWeek.addEventListener('click', function () { declareAway(7, awayWeek); });
700 if (awayMonth) awayMonth.addEventListener('click', function () { declareAway(30, awayMonth); });
701
702 function sendWave(uri, btn) {
703 btn.disabled = true;
704 fetch('/guardian/api/wave', {
705 method: 'POST', headers: { 'Content-Type': 'application/json' },
706 body: JSON.stringify({ ward: uri, site: S.site }),
707 }).then(function (r) { return r.json(); })
708 .then(function (j) { btn.disabled = false; btn.textContent = (j && j.ok) ? (T.waved || '๐Ÿ‘‹ sent') : (T.wave || '๐Ÿ‘‹ Wave'); })
709 .catch(function () { btn.disabled = false; });
710 }
711
712 function remove(uri, btn, onError) {
713 btn.disabled = true;
714 fetch('/guardian/wards/remove', {
715 method: 'POST', headers: { 'Content-Type': 'application/json' },
716 body: JSON.stringify({ uri: uri, site: S.site }),
717 }).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); })
718 .then(function (res) {
719 // The server can refuse: emptying shaer:guardians is emancipation and
720 // not one guardian's call (ยง3.4). Say so instead of silently redrawing.
721 if (!res.ok) { if (onError) onError(res.j && res.j.error); return; }
722 refresh();
723 })
724 .catch(function () { if (onError) onError('network'); else btn.disabled = false; });
725 }
726
727 function renderAll() { renderHelp(); renderPending(); renderWards(); }
728
729 // โ”€โ”€ 0. Wards' corner: read-only feed of your wards' posts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
730 // Lives inside each child's panel now, so the fetches only fill a cache and
731 // ask the ward list to redraw. A guardian watches, it does not publish.
732 function feedCard(p) {
733 var card = el('div', 'g-card feed');
734 var head = el('div', 'row');
735 head.appendChild(el('span', 'who grow', p.author));
736 if (p.published) head.appendChild(el('span', 'g-when', when(p, p.published)));
737 card.appendChild(head);
738 var body = el('div', 'feed-body');
739 // body_html is de gedeelde note-body-partial, serverside gerenderd: opmaak,
740 // media, quote-kaart en embed, precies als in de Krant en in Berichten.
741 // Valt terug op de kale content voor een client uit de cache.
742 var html = p.body_html || p.content || '';
743 if (p.cw) {
744 // De content warning blijft van de PWA zelf: note-body versluiert alleen
745 // bij nsfw, en een ward-post met alleen een cw hoort hier dicht te staan.
746 var d = document.createElement('details');
747 var sum = document.createElement('summary'); sum.textContent = p.cw; d.appendChild(sum);
748 var inner = el('div'); inner.innerHTML = html; d.appendChild(inner);
749 body.appendChild(d);
750 } else {
751 body.innerHTML = html; // server-sanitized HTML (same as Berichten)
752 }
753 card.appendChild(body);
754 return card;
755 }
756
757 function loadFeed() {
758 return fetch('/guardian/api/feed?site=' + encodeURIComponent(S.site))
759 .then(function (r) { return r.json(); })
760 .then(function (f) { if (f && !f.error) { FEED = f.items || []; renderWards(); } })
761 .catch(function () { /* panels just show "nothing yet" */ });
762 }
763
764 // โ”€โ”€ 0b. Follow requests on your wards (ยง5.3) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
765 function answerFollow(id, decision, btn) {
766 if (btn) btn.disabled = true;
767 fetch('/guardian/api/follow/' + encodeURIComponent(id), {
768 method: 'POST', headers: { 'Content-Type': 'application/json' },
769 body: JSON.stringify({ decision: decision, site: S.site }),
770 }).then(loadFollowReqs);
771 }
772 function followCard(f) {
773 var card = el('div', 'g-card');
774 var row = el('div', 'row');
775 // Inside the child's own panel the ward name is a given, so only the
776 // person asking is named here.
777 row.appendChild(el('span', 'who grow', f.follower));
778 var ok = el('button', 'small', T.accept || 'Accept');
779 ok.addEventListener('click', function () { answerFollow(f.id, 'approve', ok); });
780 var no = el('button', 'quiet small', T.reject || 'Deny');
781 no.addEventListener('click', function () { answerFollow(f.id, 'reject', no); });
782 row.appendChild(ok); row.appendChild(no);
783 card.appendChild(row);
784 return card;
785 }
786 function loadFollowReqs() {
787 return fetch('/guardian/api/follow-requests?site=' + encodeURIComponent(S.site))
788 .then(function (r) { return r.json(); })
789 .then(function (f) { if (f && !f.error) { FOLLOWS = f.items || []; renderWards(); } })
790 .catch(function () { /* panels just show "none waiting" */ });
791 }
792
793 function refresh() {
794 return fetch('/guardian/api/state?site=' + encodeURIComponent(S.site))
795 .then(function (r) { return r.json(); })
796 .then(function (s) { if (s && !s.error) { S = s; T = s.strings || T; renderAll(); } })
797 .then(loadFeed).then(loadFollowReqs);
798 }
799
800 // โ”€โ”€ 2. Adopt โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
801 var form = document.getElementById('adopt-form');
802 var input = document.getElementById('adopt-handle');
803 var adoptBtn = document.getElementById('adopt-btn');
804 var msg = document.getElementById('adopt-msg');
805 function setMsg(text, isErr) { msg.hidden = false; msg.className = 'g-msg' + (isErr ? ' err' : ''); msg.textContent = text; }
806
807 form.addEventListener('submit', function (ev) {
808 ev.preventDefault();
809 var handle = input.value.trim();
810 if (!handle) return;
811 adoptBtn.disabled = true;
812 setMsg(T.sending || 'โ€ฆ', false);
813 fetch('/guardian/adopt', {
814 method: 'POST', headers: { 'Content-Type': 'application/json' },
815 body: JSON.stringify({ handle: handle, site: S.site }),
816 }).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); })
817 .then(function (res) {
818 adoptBtn.disabled = false;
819 if (res.ok) {
820 input.value = '';
821 // Always refresh: the offer is recorded even if delivery is still
822 // in flight. Show it under "Verzonden aanvragen".
823 setMsg(res.j.delivered === false ? T.sent_retry : T.sent, false);
824 refresh();
825 } else {
826 setMsg((res.j.error === 'not_found' ? T.not_found : T.failed) , true);
827 }
828 })
829 .catch(function () { adoptBtn.disabled = false; setMsg(T.network, true); });
830 });
831
832 // โ”€โ”€ Site picker โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
833 var picker = document.getElementById('site-picker');
834 if (picker) picker.addEventListener('change', function () {
835 location.href = '/guardian?site=' + encodeURIComponent(picker.value);
836 });
837
838 // โ”€โ”€ 5. Push โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
839 var toggle = document.getElementById('push-toggle');
840 var pmsg = document.getElementById('push-msg');
841 function pushState() {
842 if (!('serviceWorker' in navigator) || !('PushManager' in window)) { toggle.disabled = true; return; }
843 navigator.serviceWorker.register('/sw.js').catch(function () {});
844 navigator.serviceWorker.ready
845 .then(function (reg) { return reg.pushManager.getSubscription(); })
846 .then(function (sub) {
847 toggle.textContent = sub ? toggle.dataset.onLabel : toggle.dataset.offLabel;
848 toggle.dataset.subscribed = sub ? '1' : '';
849 toggle.classList.toggle('is-on', !!sub);
850 });
851 }
852 function urlB64(base64) {
853 var pad = '='.repeat((4 - (base64.length % 4)) % 4);
854 var b = (base64 + pad).replace(/-/g, '+').replace(/_/g, '/');
855 var raw = atob(b); var arr = new Uint8Array(raw.length);
856 for (var i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
857 return arr;
858 }
859 toggle.addEventListener('click', function () {
860 pmsg.hidden = true;
861 navigator.serviceWorker.ready.then(function (reg) {
862 if (toggle.dataset.subscribed) {
863 reg.pushManager.getSubscription().then(function (sub) {
864 if (!sub) return;
865 fetch('/push/unsubscribe', {
866 method: 'POST', headers: { 'Content-Type': 'application/json' },
867 body: JSON.stringify({ endpoint: sub.endpoint }),
868 }).then(function () { return sub.unsubscribe(); }).then(pushState);
869 });
870 return;
871 }
872 fetch('/push/vapid').then(function (r) { return r.json(); }).then(function (v) {
873 if (!v.publicKey) throw new Error('no key');
874 return reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlB64(v.publicKey) });
875 }).then(function (sub) {
876 return fetch('/push/subscribe', {
877 method: 'POST', headers: { 'Content-Type': 'application/json' },
878 body: JSON.stringify({
879 subscription: sub.toJSON(),
880 alerts: { help: 1, guardian: 1, dm: 1, follow: 0, reply: 0, like: 0, boost: 0 },
881 uaLabel: 'Guardian PWA',
882 }),
883 });
884 }).then(pushState).catch(function (e) {
885 pmsg.hidden = false; pmsg.className = 'g-msg err';
886 pmsg.textContent = (T.push_unavailable || 'Push unavailable') + ': ' + e.message;
887 });
888 });
889 });
890
891 renderAll(); pushState(); loadFeed(); loadFollowReqs();
892
893 // De push die de melding brengt is meteen het teken dat de staat veranderd is.
894 // Daarmee hoeft er geen tweede, open verbinding bij: hetzelfde kanaal doet het
895 // werk, en het werkt ook als de app dicht is.
896 if ('serviceWorker' in navigator) {
897 navigator.serviceWorker.addEventListener('message', function (e) {
898 if (e.data && e.data.klonkt === 'push') refresh();
899 });
900 }
901 // Het tikje blijft als vangnet -- niet elke verandering geeft een melding, en
902 // niet iedereen heeft meldingen aanstaan. Maar niet tikken terwijl niemand
903 // kijkt: dat waren verzoeken voor een tabblad op de achtergrond. Bij terugkomen
904 // meteen een keer, want dan is de kans op nieuws het grootst.
905 setInterval(function () { if (!document.hidden) refresh(); }, 45000);
906 document.addEventListener('visibilitychange', function () { if (!document.hidden) refresh(); });
907 } catch (e) {
908 fatal((e && e.message) || String(e));
909 }
910})();
Note: See TracBrowser for help on using the repository browser.