source: Klonkt/src/assets/js/guardian.js@ 742ba7e

main
Last change on this file since 742ba7e was 742ba7e, checked in by Robin Genis <roboburr@…>, 6 weeks ago

Een ward loslaten vraagt eerst, en zegt wat het doet

Loslaten zat achter een window.confirm met een lap tekst erin. Dat is precies
het soort dialoog dat mensen wegklikken om ervan af te zijn, terwijl dit de
zwaarste knop in de app is. Nu is het een stap in het paneel: eerst de gevolgen,
dan ja of nee, met nee links zodat de uitweg de makkelijke is om te raken.

De waarschuwing wordt opgebouwd uit wat er echt aan de hand is, niet uit een
vaste zin, want loslaten betekent twee verschillende dingen. Blijven er andere
guardians over, dan stap jij op en blijft het kind een ward (FEP-633c 3.3). Ben
je de laatste, dan is het emancipatie, en 3.4 zegt uitdrukkelijk dat geen enkele
guardian daar alleen over gaat: drie instemmende volwassenen, of een meerderheid
met twee getuigen. Een nieuwe route zoekt dat op het moment van drukken op, want
voor een ward die wij niet hosten moet daarvoor die server bevraagd worden en
dat hoort niet bij elke ververs-ronde.

En het derde ding dat er hoorde te staan: het Undo federeert nog niet (fase 4 in
relations.js), dus vandaag haalt de knop de band alleen hier weg terwijl de
server van het kind je gewoon als guardian blijft noemen. Zonder die regel drukt
een guardian erop in de overtuiging dat het kind losgelaten is.

Changed files:
src/routes/guardian.js

  • GET /guardian/wards/release-check: telt de guardians van dit kind (lokaal uit de relaties, remote uit shaer:guardians van de actor) en weigert een ward die niet van jou is
  • de nieuwe labels toegevoegd aan uiStrings

src/assets/js/guardian.js

  • releaseStep: de waarschuwing plus ja/nee, in plaats van window.confirm
  • de site gaat mee in de check, anders kijkt de server naar de verkeerde

src/assets/css/guardian.css

  • --danger toegevoegd; die bestond niet, waardoor het kader en de knop onzichtbaar bleven
  • opmaak voor het waarschuwingsblok

src/services/i18n.js

  • 24 strings voor de waarschuwing in nl, en, de

test/guardian-panel.test.js

  • loslaten gaat in twee stappen en niet via window.confirm
  • elke CSS-variabele die de PWA gebruikt is er ook een die hij definieert

remarks: alle vier de gevallen in de browser nagelopen: laatste guardian,
meerdere guardians, onbereikbare server, en een ward die niet van je is. Nee
zet de knop terug zonder iets te doen, ja verwijdert alleen die ene relatie.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 22.2 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 function helpCard(h) {
53 var card = el('div', 'g-card help');
54 var row = el('div', 'row');
55 var who = el('span', 'who grow');
56 // name_html carries the custom emojis (FEP-9098) of the display name, the
57 // same way de Krant renders a byline. Falls back to the plain name.
58 if (h.name_html) who.innerHTML = h.name_html;
59 else who.textContent = h.actor_name || handleOf(h.actor_uri, h.actor_handle);
60 row.appendChild(who);
61 row.appendChild(el('span', 'when', when(h, h.published || h.created_at)));
62 card.appendChild(row);
63 var body = el('div', 'body g-note');
64 // body_html is the shared note-body partial, rendered server-side: the
65 // content with its emojis, the quote / link-preview card and the media.
66 // Falls back to the bare content for rows stored before that existed.
67 body.innerHTML = h.body_html || h.content || ''; // sanitized server-side on ingest
68 card.appendChild(body);
69 if (h.note_url) {
70 var a = el('a', 'g-link', T.open || 'open');
71 a.href = h.note_url; a.target = '_blank'; a.rel = 'noopener';
72 card.appendChild(a);
73 }
74 return card;
75 }
76
77 function renderHelp() {
78 var list = document.getElementById('help-list');
79 list.textContent = '';
80 var help = S.help || [];
81 help.slice(0, HELP_TOP).forEach(function (h) { list.appendChild(helpCard(h)); });
82 if (help.length > HELP_TOP) {
83 list.appendChild(el('p', 'g-sec-sub', '+ ' + (help.length - HELP_TOP) + ' — ' + (T.panel_help || '')));
84 }
85 var badge = document.getElementById('help-count');
86 badge.textContent = help.length; badge.hidden = help.length === 0;
87 show('help-empty', help.length === 0);
88 }
89
90 // ── 3. Offers I am a party to (sent, or a co-guardianship to co-approve) ─
91 function answer(offerId, decision, btn) {
92 if (btn) btn.disabled = true;
93 fetch('/guardian/offer', {
94 method: 'POST', headers: { 'Content-Type': 'application/json' },
95 body: JSON.stringify({ offer: offerId, answer: decision, site: S.site }),
96 }).then(refresh);
97 }
98 function offerCard(o) {
99 var card = el('div', 'g-card');
100 var row = el('div', 'row');
101 var subject = o['shaer:iAmCandidate']
102 ? handleOf(o['shaer:ward'], o['shaer:wardHandle']) // my sent offer: about the ward
103 : handleOf(o['shaer:candidate'], o['shaer:candidateHandle']); // co-guard: who wants in
104 row.appendChild(el('span', 'who grow', subject));
105 if (o['shaer:iAmCandidate']) {
106 // My own offer, waiting for the others to accept.
107 row.appendChild(el('span', 'tag wait', T.pending));
108 var rt = el('button', 'quiet small', T.retract);
109 rt.addEventListener('click', function () { answer(o.id, 'reject', rt); });
110 row.appendChild(rt);
111 } else if (o['shaer:needsMyAccept']) {
112 // A co-guardianship offer for a ward I already guard: my call.
113 row.appendChild(el('span', 'tag co', T.coguard));
114 var ac = el('button', 'small', T.accept);
115 ac.addEventListener('click', function () { answer(o.id, 'accept', ac); });
116 var rj = el('button', 'quiet small', T.reject);
117 rj.addEventListener('click', function () { answer(o.id, 'reject', rj); });
118 row.appendChild(ac); row.appendChild(rj);
119 } else {
120 row.appendChild(el('span', 'tag wait', T.awaiting_others));
121 }
122 card.appendChild(row);
123 return card;
124 }
125 function renderPending() {
126 var list = document.getElementById('pending-list');
127 list.textContent = '';
128 var offers = S.offers || [];
129 offers.forEach(function (o) { list.appendChild(offerCard(o)); });
130 show('pending-section', offers.length > 0);
131 }
132
133 // ── 4. Accepted wards: one panel per child ─────────────────────────────
134 // A guardian thinks per child, not per function, so everything about one
135 // child sits behind that child's row: the gated settings, the follow requests
136 // waiting on them, their calls for help, their recent posts. The row itself
137 // carries counts, so nothing that needs an answer hides inside a closed
138 // panel.
139 var openPanels = {}; // ward uri -> open, so a refresh does not close it
140 // The follow requests and the wards' posts arrive from their own endpoints
141 // and are grouped into the panels by ward, so they are cached here rather
142 // than rendered into a section of their own.
143 var FEED = [], FOLLOWS = [];
144
145 function sectionInto(panel, title, items, empty, build) {
146 var h = el('div', 'g-panel-sec');
147 h.appendChild(el('h3', null, title));
148 if (!items.length) h.appendChild(el('p', 'g-empty small', empty));
149 else items.forEach(function (it) { h.appendChild(build(it)); });
150 panel.appendChild(h);
151 return h;
152 }
153
154 function embedsButton(w) {
155 // Gated feature: external (non-fediverse) embeds. Off by default for a
156 // ward; only a guardian can open it, and the gate is enforced server-side
157 // when the feed is built, so this button is the only thing that moves it.
158 // Shown for EVERY ward, including one on another server. There the value is
159 // unknown (it lives on the ward's server), but proposing is exactly as
160 // possible: the proposal travels, the ward's server tallies the guardians
161 // and enforces. A guardian next door must not have more say than one far
162 // away.
163 var known = w.embeds === true || w.embeds === false;
164 var emb = el('button', 'quiet small',
165 (known ? (w.embeds ? T.embeds_on : T.embeds_off) : T.embeds_propose) || 'Link previews');
166 emb.addEventListener('click', function () {
167 emb.disabled = true;
168 fetch('/guardian/wards/embeds', {
169 method: 'POST', headers: { 'Content-Type': 'application/json' },
170 body: JSON.stringify({ uri: w.other_uri, allow: known ? !w.embeds : true }),
171 }).then(function (r) { return r.json(); })
172 .then(function (j) {
173 // Not settled yet: the other guardians still have to answer.
174 if (j && j.state === 'open') {
175 emb.textContent = (T.embeds_waiting || 'waiting for the other guardians');
176 emb.disabled = true;
177 return;
178 }
179 refresh();
180 })
181 .catch(function () { emb.disabled = false; });
182 });
183 return emb;
184 }
185
186 /**
187 * The second step of releasing a ward: what it does, then yes or no.
188 *
189 * The warning is assembled from what the server found, not from a fixed
190 * sentence, because releasing means two different things (FEP-633c): stepping
191 * down while other guardians remain (§3.3), or being the last one, which is
192 * emancipation and explicitly not one guardian's call (§3.4). And as long as
193 * the Undo does not federate, the ward's server keeps listing you either way
194 * — a guardian has to know that before pressing, not after.
195 */
196 function releaseStep(w, check, host, relBtn) {
197 var uri = w.other_uri;
198 var who = handleOf(uri, w.other_handle);
199 var box = el('div', 'g-warn');
200 box.appendChild(el('strong', null, (T.release_title || 'Release {who}?').replace('{who}', who)));
201 box.appendChild(el('p', null, T.release_effect || ''));
202 if (check.last === true) box.appendChild(el('p', 'grave', T.release_last || ''));
203 else if (check.last === false) box.appendChild(el('p', null, T.release_step_down || ''));
204 else box.appendChild(el('p', 'grave', T.release_unknown || ''));
205 if (check.federates === false) box.appendChild(el('p', null, T.release_local || ''));
206
207 var row = el('div', 'row');
208 var yes = el('button', 'danger small', T.release_yes || 'Yes');
209 yes.addEventListener('click', function () { yes.disabled = true; remove(uri, yes); });
210 var no = el('button', 'small', T.release_no || 'No');
211 no.addEventListener('click', function () {
212 host.removeChild(box);
213 relBtn.hidden = false; relBtn.disabled = false;
214 });
215 // No first: the way out should be the easy one to hit.
216 row.appendChild(no); row.appendChild(yes);
217 box.appendChild(row);
218 return box;
219 }
220
221 function wardPanel(w) {
222 var uri = w.other_uri;
223 var panel = el('div', 'g-panel');
224 panel.hidden = !openPanels[uri];
225
226 var set = el('div', 'g-panel-sec');
227 set.appendChild(el('h3', null, T.settings_title || 'Settings'));
228 var setRow = el('div', 'row');
229 setRow.appendChild(embedsButton(w));
230 set.appendChild(setRow);
231 panel.appendChild(set);
232
233 sectionInto(panel, T.panel_follow || 'Follow requests',
234 FOLLOWS.filter(function (f) { return f.wardUri === uri; }),
235 T.panel_follow_empty || '', followCard);
236
237 sectionInto(panel, T.panel_help || 'Calls for help',
238 (S.help || []).filter(function (h) { return h.actor_uri === uri; }),
239 T.panel_help_empty || '', helpCard);
240
241 sectionInto(panel, T.panel_posts || 'Recent posts',
242 FEED.filter(function (p) { return p.authorUri === uri; }),
243 T.panel_posts_empty || '', feedCard);
244
245 var act = el('div', 'g-panel-sec');
246 act.appendChild(el('h3', null, T.panel_actions || 'Actions'));
247 var actRow = el('div', 'row');
248 var wave = el('button', 'small', T.wave || '👋 Wave');
249 wave.addEventListener('click', function () { sendWave(uri, wave); });
250 actRow.appendChild(wave);
251 var rel = el('button', 'quiet small', T.release);
252 // Letting a child go is a decision, not a click. It opens a step that first
253 // asks the server what releasing this particular ward actually does, then
254 // says it plainly and asks yes or no. Never window.confirm: that hides a
255 // long explanation behind an OK button people press to make it go away.
256 rel.addEventListener('click', function () {
257 rel.disabled = true;
258 // site matters: with several of your own sites the server would otherwise
259 // check this ward against the wrong one and answer "not my ward".
260 fetch('/guardian/wards/release-check?site=' + encodeURIComponent(S.site) + '&uri=' + encodeURIComponent(uri))
261 .then(function (r) { return r.json(); })
262 .then(function (c) {
263 rel.hidden = true;
264 act.appendChild(releaseStep(w, c || {}, act, rel));
265 })
266 .catch(function () { rel.disabled = false; });
267 });
268 actRow.appendChild(rel);
269 act.appendChild(actRow);
270 panel.appendChild(act);
271 return panel;
272 }
273
274 function renderWards() {
275 var list = document.getElementById('wards-list');
276 list.textContent = '';
277 var wards = S.wards || [];
278 wards.forEach(function (w) {
279 var uri = w.other_uri;
280 var card = el('div', 'g-card ward');
281 var row = el('div', 'row');
282 row.appendChild(el('span', 'who grow', handleOf(uri, w.other_handle)));
283 // Counts on the row: whatever is waiting must be visible with the panel shut.
284 var nHelp = (S.help || []).filter(function (h) { return h.actor_uri === uri; }).length;
285 var nFollow = FOLLOWS.filter(function (f) { return f.wardUri === uri; }).length;
286 if (nHelp) row.appendChild(el('span', 'tag help', '🛟 ' + nHelp));
287 if (nFollow) row.appendChild(el('span', 'tag co', nFollow + ' ' + (nFollow === 1 ? (T.badge_follow_one || '') : (T.badge_follow || ''))));
288 row.appendChild(el('span', 'tag ok', T.active));
289 var toggle = el('button', 'quiet small', openPanels[uri] ? T.panel_close : T.panel_open);
290 row.appendChild(toggle);
291 card.appendChild(row);
292 var panel = wardPanel(w);
293 card.appendChild(panel);
294 toggle.addEventListener('click', function () {
295 openPanels[uri] = !openPanels[uri];
296 panel.hidden = !openPanels[uri];
297 toggle.textContent = openPanels[uri] ? T.panel_close : T.panel_open;
298 });
299 list.appendChild(card);
300 });
301 show('wards-empty', wards.length === 0);
302 }
303
304 function sendWave(uri, btn) {
305 btn.disabled = true;
306 fetch('/guardian/api/wave', {
307 method: 'POST', headers: { 'Content-Type': 'application/json' },
308 body: JSON.stringify({ ward: uri, site: S.site }),
309 }).then(function (r) { return r.json(); })
310 .then(function (j) { btn.disabled = false; btn.textContent = (j && j.ok) ? (T.waved || '👋 sent') : (T.wave || '👋 Wave'); })
311 .catch(function () { btn.disabled = false; });
312 }
313
314 function remove(uri, btn) {
315 btn.disabled = true;
316 fetch('/guardian/wards/remove', {
317 method: 'POST', headers: { 'Content-Type': 'application/json' },
318 body: JSON.stringify({ uri: uri, site: S.site }),
319 }).then(refresh);
320 }
321
322 function renderAll() { renderHelp(); renderPending(); renderWards(); }
323
324 // ── 0. Wards' corner: read-only feed of your wards' posts ───────────────
325 // Lives inside each child's panel now, so the fetches only fill a cache and
326 // ask the ward list to redraw. A guardian watches, it does not publish.
327 function feedCard(p) {
328 var card = el('div', 'g-card feed');
329 var head = el('div', 'row');
330 head.appendChild(el('span', 'who grow', p.author));
331 if (p.published) head.appendChild(el('span', 'g-when', when(p, p.published)));
332 card.appendChild(head);
333 var body = el('div', 'feed-body');
334 if (p.cw) {
335 var d = document.createElement('details');
336 var sum = document.createElement('summary'); sum.textContent = p.cw; d.appendChild(sum);
337 var inner = el('div'); inner.innerHTML = p.content || ''; d.appendChild(inner);
338 body.appendChild(d);
339 } else {
340 body.innerHTML = p.content || ''; // server-sanitized HTML (same as Berichten)
341 }
342 card.appendChild(body);
343 return card;
344 }
345
346 function loadFeed() {
347 return fetch('/guardian/api/feed?site=' + encodeURIComponent(S.site))
348 .then(function (r) { return r.json(); })
349 .then(function (f) { if (f && !f.error) { FEED = f.items || []; renderWards(); } })
350 .catch(function () { /* panels just show "nothing yet" */ });
351 }
352
353 // ── 0b. Follow requests on your wards (§5.3) ────────────────────────────
354 function answerFollow(id, decision, btn) {
355 if (btn) btn.disabled = true;
356 fetch('/guardian/api/follow/' + encodeURIComponent(id), {
357 method: 'POST', headers: { 'Content-Type': 'application/json' },
358 body: JSON.stringify({ decision: decision, site: S.site }),
359 }).then(loadFollowReqs);
360 }
361 function followCard(f) {
362 var card = el('div', 'g-card');
363 var row = el('div', 'row');
364 // Inside the child's own panel the ward name is a given, so only the
365 // person asking is named here.
366 row.appendChild(el('span', 'who grow', f.follower));
367 var ok = el('button', 'small', T.accept || 'Accept');
368 ok.addEventListener('click', function () { answerFollow(f.id, 'approve', ok); });
369 var no = el('button', 'quiet small', T.reject || 'Deny');
370 no.addEventListener('click', function () { answerFollow(f.id, 'reject', no); });
371 row.appendChild(ok); row.appendChild(no);
372 card.appendChild(row);
373 return card;
374 }
375 function loadFollowReqs() {
376 return fetch('/guardian/api/follow-requests?site=' + encodeURIComponent(S.site))
377 .then(function (r) { return r.json(); })
378 .then(function (f) { if (f && !f.error) { FOLLOWS = f.items || []; renderWards(); } })
379 .catch(function () { /* panels just show "none waiting" */ });
380 }
381
382 function refresh() {
383 return fetch('/guardian/api/state?site=' + encodeURIComponent(S.site))
384 .then(function (r) { return r.json(); })
385 .then(function (s) { if (s && !s.error) { S = s; T = s.strings || T; renderAll(); } })
386 .then(loadFeed).then(loadFollowReqs);
387 }
388
389 // ── 2. Adopt ───────────────────────────────────────────────────────────
390 var form = document.getElementById('adopt-form');
391 var input = document.getElementById('adopt-handle');
392 var adoptBtn = document.getElementById('adopt-btn');
393 var msg = document.getElementById('adopt-msg');
394 function setMsg(text, isErr) { msg.hidden = false; msg.className = 'g-msg' + (isErr ? ' err' : ''); msg.textContent = text; }
395
396 form.addEventListener('submit', function (ev) {
397 ev.preventDefault();
398 var handle = input.value.trim();
399 if (!handle) return;
400 adoptBtn.disabled = true;
401 setMsg(T.sending || '…', false);
402 fetch('/guardian/adopt', {
403 method: 'POST', headers: { 'Content-Type': 'application/json' },
404 body: JSON.stringify({ handle: handle, site: S.site }),
405 }).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); })
406 .then(function (res) {
407 adoptBtn.disabled = false;
408 if (res.ok) {
409 input.value = '';
410 // Always refresh: the offer is recorded even if delivery is still
411 // in flight. Show it under "Verzonden aanvragen".
412 setMsg(res.j.delivered === false ? T.sent_retry : T.sent, false);
413 refresh();
414 } else {
415 setMsg((res.j.error === 'not_found' ? T.not_found : T.failed) , true);
416 }
417 })
418 .catch(function () { adoptBtn.disabled = false; setMsg(T.network, true); });
419 });
420
421 // ── Site picker ────────────────────────────────────────────────────────
422 var picker = document.getElementById('site-picker');
423 if (picker) picker.addEventListener('change', function () {
424 location.href = '/guardian?site=' + encodeURIComponent(picker.value);
425 });
426
427 // ── 5. Push ────────────────────────────────────────────────────────────
428 var toggle = document.getElementById('push-toggle');
429 var pmsg = document.getElementById('push-msg');
430 function pushState() {
431 if (!('serviceWorker' in navigator) || !('PushManager' in window)) { toggle.disabled = true; return; }
432 navigator.serviceWorker.register('/sw.js').catch(function () {});
433 navigator.serviceWorker.ready
434 .then(function (reg) { return reg.pushManager.getSubscription(); })
435 .then(function (sub) {
436 toggle.textContent = sub ? toggle.dataset.onLabel : toggle.dataset.offLabel;
437 toggle.dataset.subscribed = sub ? '1' : '';
438 toggle.classList.toggle('is-on', !!sub);
439 });
440 }
441 function urlB64(base64) {
442 var pad = '='.repeat((4 - (base64.length % 4)) % 4);
443 var b = (base64 + pad).replace(/-/g, '+').replace(/_/g, '/');
444 var raw = atob(b); var arr = new Uint8Array(raw.length);
445 for (var i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
446 return arr;
447 }
448 toggle.addEventListener('click', function () {
449 pmsg.hidden = true;
450 navigator.serviceWorker.ready.then(function (reg) {
451 if (toggle.dataset.subscribed) {
452 reg.pushManager.getSubscription().then(function (sub) {
453 if (!sub) return;
454 fetch('/push/unsubscribe', {
455 method: 'POST', headers: { 'Content-Type': 'application/json' },
456 body: JSON.stringify({ endpoint: sub.endpoint }),
457 }).then(function () { return sub.unsubscribe(); }).then(pushState);
458 });
459 return;
460 }
461 fetch('/push/vapid').then(function (r) { return r.json(); }).then(function (v) {
462 if (!v.publicKey) throw new Error('no key');
463 return reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlB64(v.publicKey) });
464 }).then(function (sub) {
465 return fetch('/push/subscribe', {
466 method: 'POST', headers: { 'Content-Type': 'application/json' },
467 body: JSON.stringify({
468 subscription: sub.toJSON(),
469 alerts: { help: 1, guardian: 1, dm: 1, follow: 0, reply: 0, like: 0, boost: 0 },
470 uaLabel: 'Guardian PWA',
471 }),
472 });
473 }).then(pushState).catch(function (e) {
474 pmsg.hidden = false; pmsg.className = 'g-msg err';
475 pmsg.textContent = (T.push_unavailable || 'Push unavailable') + ': ' + e.message;
476 });
477 });
478 });
479
480 renderAll(); pushState(); loadFeed(); loadFollowReqs();
481 setInterval(refresh, 45000); // live-ish while open
482 } catch (e) {
483 fatal((e && e.message) || String(e));
484 }
485})();
Note: See TracBrowser for help on using the repository browser.