Changeset 70677e96 in Klonkt for src/assets/js
- Timestamp:
- 07/28/2026 07:43:49 PM (6 weeks ago)
- Branches:
- main
- Children:
- 742ba7e
- Parents:
- a7bcf66
- File:
-
- 1 edited
-
src/assets/js/guardian.js (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/js/guardian.js
ra7bcf66 r70677e96 43 43 44 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 45 77 function renderHelp() { 46 78 var list = document.getElementById('help-list'); 47 79 list.textContent = ''; 48 80 var help = S.help || []; 49 help.forEach(function (h) { 50 var card = el('div', 'g-card help'); 51 var row = el('div', 'row'); 52 var who = el('span', 'who grow'); 53 // name_html carries the custom emojis (FEP-9098) of the display name, the 54 // same way de Krant renders a byline. Falls back to the plain name. 55 if (h.name_html) who.innerHTML = h.name_html; 56 else who.textContent = h.actor_name || handleOf(h.actor_uri, h.actor_handle); 57 row.appendChild(who); 58 row.appendChild(el('span', 'when', when(h, h.published || h.created_at))); 59 card.appendChild(row); 60 var body = el('div', 'body g-note'); 61 // body_html is the shared note-body partial, rendered server-side: the 62 // content with its emojis, the quote / link-preview card and the media. 63 // Falls back to the bare content for rows stored before that existed. 64 body.innerHTML = h.body_html || h.content || ''; // sanitized server-side on ingest 65 card.appendChild(body); 66 if (h.note_url) { 67 var a = el('a', 'g-link', T.open || 'open'); 68 a.href = h.note_url; a.target = '_blank'; a.rel = 'noopener'; 69 card.appendChild(a); 70 } 71 list.appendChild(card); 72 }); 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 } 73 85 var badge = document.getElementById('help-count'); 74 86 badge.textContent = help.length; badge.hidden = help.length === 0; … … 119 131 } 120 132 121 // ── 4. Accepted wards ────────────────────────────────────────────────── 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 function wardPanel(w) { 187 var uri = w.other_uri; 188 var panel = el('div', 'g-panel'); 189 panel.hidden = !openPanels[uri]; 190 191 var set = el('div', 'g-panel-sec'); 192 set.appendChild(el('h3', null, T.settings_title || 'Settings')); 193 var setRow = el('div', 'row'); 194 setRow.appendChild(embedsButton(w)); 195 set.appendChild(setRow); 196 panel.appendChild(set); 197 198 sectionInto(panel, T.panel_follow || 'Follow requests', 199 FOLLOWS.filter(function (f) { return f.wardUri === uri; }), 200 T.panel_follow_empty || '', followCard); 201 202 sectionInto(panel, T.panel_help || 'Calls for help', 203 (S.help || []).filter(function (h) { return h.actor_uri === uri; }), 204 T.panel_help_empty || '', helpCard); 205 206 sectionInto(panel, T.panel_posts || 'Recent posts', 207 FEED.filter(function (p) { return p.authorUri === uri; }), 208 T.panel_posts_empty || '', feedCard); 209 210 var act = el('div', 'g-panel-sec'); 211 act.appendChild(el('h3', null, T.panel_actions || 'Actions')); 212 var actRow = el('div', 'row'); 213 var wave = el('button', 'small', T.wave || '👋 Wave'); 214 wave.addEventListener('click', function () { sendWave(uri, wave); }); 215 actRow.appendChild(wave); 216 var rel = el('button', 'quiet small', T.release); 217 // Releasing a ward is heavy and hard to undo (coming back needs a fresh 218 // offer the ward accepts), so it asks first and spells out what changes. 219 rel.addEventListener('click', function () { 220 var who = handleOf(uri, w.other_handle); 221 var msg = (T.release_confirm || 'Release {who}?').replace('{who}', who); 222 if (window.confirm(msg)) remove(uri, rel); 223 }); 224 actRow.appendChild(rel); 225 act.appendChild(actRow); 226 panel.appendChild(act); 227 return panel; 228 } 229 122 230 function renderWards() { 123 231 var list = document.getElementById('wards-list'); … … 125 233 var wards = S.wards || []; 126 234 wards.forEach(function (w) { 127 var card = el('div', 'g-card'); 235 var uri = w.other_uri; 236 var card = el('div', 'g-card ward'); 128 237 var row = el('div', 'row'); 129 row.appendChild(el('span', 'who grow', handleOf(w.other_uri, w.other_handle))); 238 row.appendChild(el('span', 'who grow', handleOf(uri, w.other_handle))); 239 // Counts on the row: whatever is waiting must be visible with the panel shut. 240 var nHelp = (S.help || []).filter(function (h) { return h.actor_uri === uri; }).length; 241 var nFollow = FOLLOWS.filter(function (f) { return f.wardUri === uri; }).length; 242 if (nHelp) row.appendChild(el('span', 'tag help', '🛟 ' + nHelp)); 243 if (nFollow) row.appendChild(el('span', 'tag co', nFollow + ' ' + (nFollow === 1 ? (T.badge_follow_one || '') : (T.badge_follow || '')))); 130 244 row.appendChild(el('span', 'tag ok', T.active)); 131 var wave = el('button', 'small', T.wave || '👋 Wave'); 132 wave.addEventListener('click', function () { sendWave(w.other_uri, wave); }); 133 row.appendChild(wave); 134 // Gated feature: external (non-fediverse) embeds. Off by default for a 135 // ward; only a guardian can open it, and the gate is enforced server-side 136 // when the feed is built, so this button is the only thing that moves it. 137 // Shown for EVERY ward, including one on another server. There the value 138 // is unknown (it lives on the ward's server), but proposing is exactly as 139 // possible: the proposal travels, the ward's server tallies the guardians 140 // and enforces. A guardian next door must not have more say than one far 141 // away. 142 var known = w.embeds === true || w.embeds === false; 143 var emb = el('button', 'quiet small', 144 (known ? (w.embeds ? T.embeds_on : T.embeds_off) : T.embeds_propose) || 'Link previews'); 145 emb.addEventListener('click', function () { 146 emb.disabled = true; 147 fetch('/guardian/wards/embeds', { 148 method: 'POST', headers: { 'Content-Type': 'application/json' }, 149 body: JSON.stringify({ uri: w.other_uri, allow: known ? !w.embeds : true }), 150 }).then(function (r) { return r.json(); }) 151 .then(function (j) { 152 // Not settled yet: the other guardians still have to answer. 153 if (j && j.state === 'open') { 154 emb.textContent = (T.embeds_waiting || 'waiting for the other guardians'); 155 emb.disabled = true; 156 return; 157 } 158 refresh(); 159 }) 160 .catch(function () { emb.disabled = false; }); 245 var toggle = el('button', 'quiet small', openPanels[uri] ? T.panel_close : T.panel_open); 246 row.appendChild(toggle); 247 card.appendChild(row); 248 var panel = wardPanel(w); 249 card.appendChild(panel); 250 toggle.addEventListener('click', function () { 251 openPanels[uri] = !openPanels[uri]; 252 panel.hidden = !openPanels[uri]; 253 toggle.textContent = openPanels[uri] ? T.panel_close : T.panel_open; 161 254 }); 162 row.appendChild(emb);163 var btn = el('button', 'quiet small', T.release);164 // Releasing a ward is heavy and hard to undo (coming back needs a fresh165 // offer the ward accepts), so it asks first and spells out what changes.166 btn.addEventListener('click', function () {167 var who = handleOf(w.other_uri, w.other_handle);168 var msg = (T.release_confirm || 'Release {who}?').replace('{who}', who);169 if (window.confirm(msg)) remove(w.other_uri, btn);170 });171 row.appendChild(btn);172 card.appendChild(row);173 255 list.appendChild(card); 174 256 }); … … 197 279 198 280 // ── 0. Wards' corner: read-only feed of your wards' posts ─────────────── 199 function renderFeed(items) { 200 var list = document.getElementById('feed-list'); 201 list.textContent = ''; 202 (items || []).forEach(function (p) { 203 var card = el('div', 'g-card feed'); 204 var head = el('div', 'row'); 205 head.appendChild(el('span', 'who grow', p.author)); 206 if (p.published) head.appendChild(el('span', 'g-when', when(p, p.published))); 207 card.appendChild(head); 208 var body = el('div', 'feed-body'); 209 if (p.cw) { 210 var d = document.createElement('details'); 211 var sum = document.createElement('summary'); sum.textContent = p.cw; d.appendChild(sum); 212 var inner = el('div'); inner.innerHTML = p.content || ''; d.appendChild(inner); 213 body.appendChild(d); 214 } else { 215 body.innerHTML = p.content || ''; // server-sanitized HTML (same as Berichten) 216 } 217 card.appendChild(body); 218 list.appendChild(card); 219 }); 220 show('feed-section', (items || []).length > 0); 281 // Lives inside each child's panel now, so the fetches only fill a cache and 282 // ask the ward list to redraw. A guardian watches, it does not publish. 283 function feedCard(p) { 284 var card = el('div', 'g-card feed'); 285 var head = el('div', 'row'); 286 head.appendChild(el('span', 'who grow', p.author)); 287 if (p.published) head.appendChild(el('span', 'g-when', when(p, p.published))); 288 card.appendChild(head); 289 var body = el('div', 'feed-body'); 290 if (p.cw) { 291 var d = document.createElement('details'); 292 var sum = document.createElement('summary'); sum.textContent = p.cw; d.appendChild(sum); 293 var inner = el('div'); inner.innerHTML = p.content || ''; d.appendChild(inner); 294 body.appendChild(d); 295 } else { 296 body.innerHTML = p.content || ''; // server-sanitized HTML (same as Berichten) 297 } 298 card.appendChild(body); 299 return card; 221 300 } 222 301 … … 224 303 return fetch('/guardian/api/feed?site=' + encodeURIComponent(S.site)) 225 304 .then(function (r) { return r.json(); }) 226 .then(function (f) { if (f && !f.error) renderFeed(f.items);})227 .catch(function () { /* corner just stays hidden*/ });305 .then(function (f) { if (f && !f.error) { FEED = f.items || []; renderWards(); } }) 306 .catch(function () { /* panels just show "nothing yet" */ }); 228 307 } 229 308 … … 236 315 }).then(loadFollowReqs); 237 316 } 238 function renderFollowReqs(items) { 239 var list = document.getElementById('follow-list'); 240 list.textContent = ''; 241 (items || []).forEach(function (f) { 242 var card = el('div', 'g-card'); 243 var row = el('div', 'row'); 244 row.appendChild(el('span', 'who grow', f.follower + ' → ' + f.ward)); 245 var ok = el('button', 'small', T.accept || 'Accept'); 246 ok.addEventListener('click', function () { answerFollow(f.id, 'approve', ok); }); 247 var no = el('button', 'quiet small', T.reject || 'Deny'); 248 no.addEventListener('click', function () { answerFollow(f.id, 'reject', no); }); 249 row.appendChild(ok); row.appendChild(no); 250 card.appendChild(row); 251 list.appendChild(card); 252 }); 253 show('follow-section', (items || []).length > 0); 317 function followCard(f) { 318 var card = el('div', 'g-card'); 319 var row = el('div', 'row'); 320 // Inside the child's own panel the ward name is a given, so only the 321 // person asking is named here. 322 row.appendChild(el('span', 'who grow', f.follower)); 323 var ok = el('button', 'small', T.accept || 'Accept'); 324 ok.addEventListener('click', function () { answerFollow(f.id, 'approve', ok); }); 325 var no = el('button', 'quiet small', T.reject || 'Deny'); 326 no.addEventListener('click', function () { answerFollow(f.id, 'reject', no); }); 327 row.appendChild(ok); row.appendChild(no); 328 card.appendChild(row); 329 return card; 254 330 } 255 331 function loadFollowReqs() { 256 332 return fetch('/guardian/api/follow-requests?site=' + encodeURIComponent(S.site)) 257 333 .then(function (r) { return r.json(); }) 258 .then(function (f) { if (f && !f.error) renderFollowReqs(f.items);})259 .catch(function () { /* stays hidden*/ });334 .then(function (f) { if (f && !f.error) { FOLLOWS = f.items || []; renderWards(); } }) 335 .catch(function () { /* panels just show "none waiting" */ }); 260 336 } 261 337
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)