source: Klonkt/src/assets/js/guardian.js@ c628dcd4

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

Paid-post pagina krijgt de normale header, en Release a Ward vraagt eerst

Twee kleine dingen uit de backlog.

shaer-eaf: de pagina-s in de unlock-flow (paid-result en paid-passkey) stonden op
bodyClass on-special. Die klasse is bedoeld voor de fediverse/beheer-achtige
pagina-s en zet in de topnav dezelfde behandeling aan als bij admin: een kale
terug-naar-site link i.p.v. de site-header. Maar dit zijn gewoon bezoekerspagina-s
in het midden van een aankoop. Nu on-post, wat de normale (compacte) header geeft.

shaer-o4a: een ward loslaten ging met een enkele tik, zonder vraag. Dat is een
zware en slecht terug te draaien actie: je stopt als guardian, ziet hun berichten
niet meer, krijgt geen hulpverzoeken meer en beslist niet meer over volgverzoeken,
en terugkomen kan alleen met een nieuwe aanvraag die de ward accepteert. Er komt
nu een bevestiging die precies dat opsomt, in alle drie de talen.

Changed files:
src/routes/paid.js

  • vier renders van on-special naar on-post (paid-result en paid-passkey)

src/services/i18n.js

  • guardian.release_confirm toegevoegd in nl, en, de

src/routes/guardian.js

  • release_confirm meegegeven aan de PWA-strings

src/assets/js/guardian.js

  • Loslaten vraagt eerst om bevestiging, met de handle in de tekst

remarks: 190 tests groen. De paid-flow is hier niet live te reproduceren (die
routes 404-en zonder Patreon-config), dus dat deel is via de topnav-logica
geverifieerd en niet in de browser.

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

  • Property mode set to 100644
File size: 14.4 KB
RevLine 
[c26cc18]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). */
[318d0c2]4(function () {
5 'use strict';
[fcd6964]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 {
[c26cc18]20 var S = JSON.parse(document.getElementById('guardian-state').textContent || '{}');
21 var T = S.strings || {};
[318d0c2]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) {
[fcd6964]30 if (cached && cached.charAt(0) === '@') return cached; // trust only real @handles
[318d0c2]31 try { var u = new URL(uri); return '@' + u.pathname.split('/').filter(Boolean).pop() + '@' + u.host; }
32 catch (e) { return uri; }
33 }
[c26cc18]34 function when(s) { return String(s || '').slice(0, 16).replace('T', ' '); }
35 function show(id, on) { document.getElementById(id).hidden = !on; }
[318d0c2]36
[c26cc18]37 // ── 1. Help requests ───────────────────────────────────────────────────
[318d0c2]38 function renderHelp() {
39 var list = document.getElementById('help-list');
40 list.textContent = '';
[c26cc18]41 var help = S.help || [];
42 help.forEach(function (h) {
[318d0c2]43 var card = el('div', 'g-card help');
44 var row = el('div', 'row');
[c26cc18]45 row.appendChild(el('span', 'who grow', h.actor_name || handleOf(h.actor_uri, h.actor_handle)));
46 row.appendChild(el('span', 'when', when(h.published || h.created_at)));
[318d0c2]47 card.appendChild(row);
48 var body = el('div', 'body');
[c26cc18]49 body.innerHTML = h.content || ''; // sanitized server-side on ingest
[318d0c2]50 card.appendChild(body);
51 if (h.note_url) {
[c26cc18]52 var a = el('a', 'g-link', T.open || 'open');
53 a.href = h.note_url; a.target = '_blank'; a.rel = 'noopener';
54 card.appendChild(a);
[318d0c2]55 }
56 list.appendChild(card);
57 });
[c26cc18]58 var badge = document.getElementById('help-count');
59 badge.textContent = help.length; badge.hidden = help.length === 0;
60 show('help-empty', help.length === 0);
[318d0c2]61 }
62
[780a7c6]63 // ── 3. Offers I am a party to (sent, or a co-guardianship to co-approve) ─
64 function answer(offerId, decision, btn) {
65 if (btn) btn.disabled = true;
66 fetch('/guardian/offer', {
67 method: 'POST', headers: { 'Content-Type': 'application/json' },
68 body: JSON.stringify({ offer: offerId, answer: decision, site: S.site }),
69 }).then(refresh);
70 }
71 function offerCard(o) {
72 var card = el('div', 'g-card');
73 var row = el('div', 'row');
74 var subject = o['shaer:iAmCandidate']
75 ? handleOf(o['shaer:ward'], o['shaer:wardHandle']) // my sent offer: about the ward
76 : handleOf(o['shaer:candidate'], o['shaer:candidateHandle']); // co-guard: who wants in
77 row.appendChild(el('span', 'who grow', subject));
78 if (o['shaer:iAmCandidate']) {
79 // My own offer, waiting for the others to accept.
80 row.appendChild(el('span', 'tag wait', T.pending));
81 var rt = el('button', 'quiet small', T.retract);
82 rt.addEventListener('click', function () { answer(o.id, 'reject', rt); });
83 row.appendChild(rt);
84 } else if (o['shaer:needsMyAccept']) {
85 // A co-guardianship offer for a ward I already guard: my call.
86 row.appendChild(el('span', 'tag co', T.coguard));
87 var ac = el('button', 'small', T.accept);
88 ac.addEventListener('click', function () { answer(o.id, 'accept', ac); });
89 var rj = el('button', 'quiet small', T.reject);
90 rj.addEventListener('click', function () { answer(o.id, 'reject', rj); });
91 row.appendChild(ac); row.appendChild(rj);
92 } else {
93 row.appendChild(el('span', 'tag wait', T.awaiting_others));
94 }
95 card.appendChild(row);
96 return card;
97 }
[c26cc18]98 function renderPending() {
99 var list = document.getElementById('pending-list');
100 list.textContent = '';
[780a7c6]101 var offers = S.offers || [];
102 offers.forEach(function (o) { list.appendChild(offerCard(o)); });
103 show('pending-section', offers.length > 0);
[318d0c2]104 }
[c26cc18]105
106 // ── 4. Accepted wards ──────────────────────────────────────────────────
[318d0c2]107 function renderWards() {
[c26cc18]108 var list = document.getElementById('wards-list');
109 list.textContent = '';
110 var wards = S.wards || [];
111 wards.forEach(function (w) {
112 var card = el('div', 'g-card');
113 var row = el('div', 'row');
114 row.appendChild(el('span', 'who grow', handleOf(w.other_uri, w.other_handle)));
115 row.appendChild(el('span', 'tag ok', T.active));
[f1c50f9]116 var wave = el('button', 'small', T.wave || '👋 Wave');
117 wave.addEventListener('click', function () { sendWave(w.other_uri, wave); });
118 row.appendChild(wave);
[c26cc18]119 var btn = el('button', 'quiet small', T.release);
[c628dcd4]120 // Releasing a ward is heavy and hard to undo (coming back needs a fresh
121 // offer the ward accepts), so it asks first and spells out what changes.
122 btn.addEventListener('click', function () {
123 var who = handleOf(w.other_uri, w.other_handle);
124 var msg = (T.release_confirm || 'Release {who}?').replace('{who}', who);
125 if (window.confirm(msg)) remove(w.other_uri, btn);
126 });
[c26cc18]127 row.appendChild(btn);
128 card.appendChild(row);
129 list.appendChild(card);
130 });
131 show('wards-empty', wards.length === 0);
132 }
133
[f1c50f9]134 function sendWave(uri, btn) {
135 btn.disabled = true;
136 fetch('/guardian/api/wave', {
137 method: 'POST', headers: { 'Content-Type': 'application/json' },
138 body: JSON.stringify({ ward: uri, site: S.site }),
139 }).then(function (r) { return r.json(); })
140 .then(function (j) { btn.disabled = false; btn.textContent = (j && j.ok) ? (T.waved || '👋 sent') : (T.wave || '👋 Wave'); })
141 .catch(function () { btn.disabled = false; });
142 }
143
[c26cc18]144 function remove(uri, btn) {
145 btn.disabled = true;
146 fetch('/guardian/wards/remove', {
147 method: 'POST', headers: { 'Content-Type': 'application/json' },
148 body: JSON.stringify({ uri: uri, site: S.site }),
149 }).then(refresh);
[318d0c2]150 }
151
[c26cc18]152 function renderAll() { renderHelp(); renderPending(); renderWards(); }
153
[f1c50f9]154 // ── 0. Wards' corner: read-only feed of your wards' posts ───────────────
155 function renderFeed(items) {
156 var list = document.getElementById('feed-list');
157 list.textContent = '';
158 (items || []).forEach(function (p) {
159 var card = el('div', 'g-card feed');
160 var head = el('div', 'row');
161 head.appendChild(el('span', 'who grow', p.author));
162 if (p.published) head.appendChild(el('span', 'g-when', when(p.published)));
163 card.appendChild(head);
164 var body = el('div', 'feed-body');
165 if (p.cw) {
166 var d = document.createElement('details');
167 var sum = document.createElement('summary'); sum.textContent = p.cw; d.appendChild(sum);
168 var inner = el('div'); inner.innerHTML = p.content || ''; d.appendChild(inner);
169 body.appendChild(d);
170 } else {
171 body.innerHTML = p.content || ''; // server-sanitized HTML (same as Berichten)
172 }
173 card.appendChild(body);
174 list.appendChild(card);
175 });
176 show('feed-section', (items || []).length > 0);
177 }
178
179 function loadFeed() {
180 return fetch('/guardian/api/feed?site=' + encodeURIComponent(S.site))
181 .then(function (r) { return r.json(); })
182 .then(function (f) { if (f && !f.error) renderFeed(f.items); })
183 .catch(function () { /* corner just stays hidden */ });
184 }
185
186 // ── 0b. Follow requests on your wards (§5.3) ────────────────────────────
187 function answerFollow(id, decision, btn) {
188 if (btn) btn.disabled = true;
189 fetch('/guardian/api/follow/' + encodeURIComponent(id), {
190 method: 'POST', headers: { 'Content-Type': 'application/json' },
191 body: JSON.stringify({ decision: decision, site: S.site }),
192 }).then(loadFollowReqs);
193 }
194 function renderFollowReqs(items) {
195 var list = document.getElementById('follow-list');
196 list.textContent = '';
197 (items || []).forEach(function (f) {
198 var card = el('div', 'g-card');
199 var row = el('div', 'row');
200 row.appendChild(el('span', 'who grow', f.follower + ' → ' + f.ward));
201 var ok = el('button', 'small', T.accept || 'Accept');
202 ok.addEventListener('click', function () { answerFollow(f.id, 'approve', ok); });
203 var no = el('button', 'quiet small', T.reject || 'Deny');
204 no.addEventListener('click', function () { answerFollow(f.id, 'reject', no); });
205 row.appendChild(ok); row.appendChild(no);
206 card.appendChild(row);
207 list.appendChild(card);
208 });
209 show('follow-section', (items || []).length > 0);
210 }
211 function loadFollowReqs() {
212 return fetch('/guardian/api/follow-requests?site=' + encodeURIComponent(S.site))
213 .then(function (r) { return r.json(); })
214 .then(function (f) { if (f && !f.error) renderFollowReqs(f.items); })
215 .catch(function () { /* stays hidden */ });
216 }
217
[318d0c2]218 function refresh() {
[c26cc18]219 return fetch('/guardian/api/state?site=' + encodeURIComponent(S.site))
[318d0c2]220 .then(function (r) { return r.json(); })
[f1c50f9]221 .then(function (s) { if (s && !s.error) { S = s; T = s.strings || T; renderAll(); } })
222 .then(loadFeed).then(loadFollowReqs);
[318d0c2]223 }
224
[c26cc18]225 // ── 2. Adopt ───────────────────────────────────────────────────────────
226 var form = document.getElementById('adopt-form');
227 var input = document.getElementById('adopt-handle');
228 var adoptBtn = document.getElementById('adopt-btn');
229 var msg = document.getElementById('adopt-msg');
230 function setMsg(text, isErr) { msg.hidden = false; msg.className = 'g-msg' + (isErr ? ' err' : ''); msg.textContent = text; }
231
232 form.addEventListener('submit', function (ev) {
[318d0c2]233 ev.preventDefault();
234 var handle = input.value.trim();
235 if (!handle) return;
[c26cc18]236 adoptBtn.disabled = true;
237 setMsg(T.sending || '…', false);
[318d0c2]238 fetch('/guardian/adopt', {
239 method: 'POST', headers: { 'Content-Type': 'application/json' },
[c26cc18]240 body: JSON.stringify({ handle: handle, site: S.site }),
[318d0c2]241 }).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); })
242 .then(function (res) {
[c26cc18]243 adoptBtn.disabled = false;
244 if (res.ok) {
245 input.value = '';
246 // Always refresh: the offer is recorded even if delivery is still
247 // in flight. Show it under "Verzonden aanvragen".
248 setMsg(res.j.delivered === false ? T.sent_retry : T.sent, false);
249 refresh();
250 } else {
251 setMsg((res.j.error === 'not_found' ? T.not_found : T.failed) , true);
252 }
[318d0c2]253 })
[c26cc18]254 .catch(function () { adoptBtn.disabled = false; setMsg(T.network, true); });
[318d0c2]255 });
256
257 // ── Site picker ────────────────────────────────────────────────────────
258 var picker = document.getElementById('site-picker');
259 if (picker) picker.addEventListener('change', function () {
260 location.href = '/guardian?site=' + encodeURIComponent(picker.value);
261 });
262
[c26cc18]263 // ── 5. Push ────────────────────────────────────────────────────────────
[318d0c2]264 var toggle = document.getElementById('push-toggle');
265 var pmsg = document.getElementById('push-msg');
266 function pushState() {
267 if (!('serviceWorker' in navigator) || !('PushManager' in window)) { toggle.disabled = true; return; }
268 navigator.serviceWorker.register('/sw.js').catch(function () {});
269 navigator.serviceWorker.ready
270 .then(function (reg) { return reg.pushManager.getSubscription(); })
271 .then(function (sub) {
272 toggle.textContent = sub ? toggle.dataset.onLabel : toggle.dataset.offLabel;
273 toggle.dataset.subscribed = sub ? '1' : '';
[c26cc18]274 toggle.classList.toggle('is-on', !!sub);
[318d0c2]275 });
276 }
277 function urlB64(base64) {
278 var pad = '='.repeat((4 - (base64.length % 4)) % 4);
279 var b = (base64 + pad).replace(/-/g, '+').replace(/_/g, '/');
280 var raw = atob(b); var arr = new Uint8Array(raw.length);
281 for (var i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
282 return arr;
283 }
284 toggle.addEventListener('click', function () {
285 pmsg.hidden = true;
286 navigator.serviceWorker.ready.then(function (reg) {
287 if (toggle.dataset.subscribed) {
288 reg.pushManager.getSubscription().then(function (sub) {
289 if (!sub) return;
290 fetch('/push/unsubscribe', {
291 method: 'POST', headers: { 'Content-Type': 'application/json' },
292 body: JSON.stringify({ endpoint: sub.endpoint }),
293 }).then(function () { return sub.unsubscribe(); }).then(pushState);
294 });
295 return;
296 }
297 fetch('/push/vapid').then(function (r) { return r.json(); }).then(function (v) {
298 if (!v.publicKey) throw new Error('no key');
299 return reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlB64(v.publicKey) });
300 }).then(function (sub) {
301 return fetch('/push/subscribe', {
302 method: 'POST', headers: { 'Content-Type': 'application/json' },
303 body: JSON.stringify({
304 subscription: sub.toJSON(),
305 alerts: { help: 1, guardian: 1, dm: 1, follow: 0, reply: 0, like: 0, boost: 0 },
306 uaLabel: 'Guardian PWA',
307 }),
308 });
309 }).then(pushState).catch(function (e) {
[c26cc18]310 pmsg.hidden = false; pmsg.className = 'g-msg err';
311 pmsg.textContent = (T.push_unavailable || 'Push unavailable') + ': ' + e.message;
[318d0c2]312 });
313 });
314 });
315
[f1c50f9]316 renderAll(); pushState(); loadFeed(); loadFollowReqs();
[c26cc18]317 setInterval(refresh, 45000); // live-ish while open
[fcd6964]318 } catch (e) {
319 fatal((e && e.message) || String(e));
320 }
[318d0c2]321})();
Note: See TracBrowser for help on using the repository browser.