Changeset 70677e96 in Klonkt for src/assets
- Timestamp:
- 07/28/2026 07:43:49 PM (6 weeks ago)
- Branches:
- main
- Children:
- 742ba7e
- Parents:
- a7bcf66
- Location:
- src/assets
- Files:
-
- 2 edited
-
css/guardian.css (modified) (1 diff)
-
js/guardian.js (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/css/guardian.css
ra7bcf66 r70677e96 118 118 .g-note .tl-media-video, .g-note .tl-media-audio { width: 100%; margin: 8px 0 0; border-radius: 10px; display: block; } 119 119 .g-note .tl-media-video { max-height: 320px; background: #000; } 120 121 /* ── Paneel per ward ─────────────────────────────────────────────────────── 122 Een guardian denkt per kind, niet per functie: achter de regel van een kind 123 zit alles over dat kind. De regel zelf draagt de tellers, zodat een dicht 124 paneel nooit iets verbergt dat een antwoord nodig heeft. */ 125 .g-card.ward > .row { gap: 8px; } 126 .g-panel { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); } 127 .g-panel[hidden] { display: none; } 128 .g-panel-sec + .g-panel-sec { margin-top: 16px; } 129 .g-panel-sec h3 { margin: 0 0 6px; font-size: .82rem; font-weight: 600; letter-spacing: .03em; 130 text-transform: uppercase; color: var(--sub); } 131 .g-panel-sec .g-card { background: transparent; border-color: var(--line); } 132 .g-panel-sec .g-card + .g-card { margin-top: 6px; } 133 .g-panel-sec .row { flex-wrap: wrap; } 134 .g-empty.small { margin: 0; font-size: .85rem; } 135 .tag.help { color: var(--accent); border-color: var(--accent); } -
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)