Ignore:
File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/assets/js/guardian.js

    r1ff801d rd56d471  
    4949  // child's panel.
    5050  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; }
    6551
    6652  function helpCard(h) {
     
    8672      card.appendChild(a);
    8773    }
    88     card.appendChild(helpState(h));
    8974    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; });
    16475  }
    16576
     
    16778    var list = document.getElementById('help-list');
    16879    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 
     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    }
    20085    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);
     86    badge.textContent = help.length; badge.hidden = help.length === 0;
     87    show('help-empty', help.length === 0);
    20588  }
    20689
     
    468351  }
    469352
    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 
    530353  function wardPanel(w) {
    531354    var uri = w.other_uri;
     
    535358    var set = el('div', 'g-panel-sec');
    536359    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     }
     360    var setRow = el('div', 'row');
     361    setRow.appendChild(gateButton(w, 'shaer:externalEmbeds', w.embeds, T.embeds_propose, T.embeds_on, T.embeds_off));
     362    // The heavier sibling (5.6): seeing that a video exists is one decision,
     363    // letting a third party's player run inside the app is another. Hidden
     364    // only when previews are known-OFF: for a ward on another server the
     365    // value is unknown, and unknown is not off. Hiding it there meant a
     366    // guardian elsewhere could never even propose playback, which is how a
     367    // whole proposal round went into the wrong gate. The ward's server
     368    // enforces play-needs-previews at serve time regardless.
     369    if (w.embeds !== false) {
     370      setRow.appendChild(gateButton(w, 'shaer:externalPlayback', w.playback, T.play_propose, T.play_on, T.play_off));
     371    }
     372    set.appendChild(setRow);
     373    // What this guardian proposed and how it stands (5.6). This used to be a
     374    // button caption that vanished on refresh, so a running decision was
     375    // invisible: you could not tell "waiting", "done" and "expired" apart.
     376    (w.proposals || []).forEach(function (p) {
     377      var what = p.feature === 'shaer:externalPlayback' ? (T.prop_play || 'playback') : (T.prop_embeds || 'link previews');
     378      var line = (T.prop_line || 'Proposal {what} {value}: {status}')
     379        .replace('{what}', what)
     380        .replace('{value}', p.value ? (T.prop_on || 'on') : (T.prop_off || 'off'))
     381        .replace('{status}', T['prop_st_' + p.status] || p.status);
     382      set.appendChild(el('p', 'small g-prop g-prop-' + p.status, line));
     383    });
    558384    panel.appendChild(set);
    559385
     
    594420      T.panel_follow_empty || '', followCard);
    595421
    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.
    599422    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); }),
     423      (S.help || []).filter(function (h) { return h.actor_uri === uri; }),
    602424      T.panel_help_empty || '', helpCard);
    603425
     
    631453    actRow.appendChild(rel);
    632454    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);
     455    panel.appendChild(act);
    638456    return panel;
    639457  }
     
    649467      row.appendChild(el('span', 'who grow', handleOf(uri, w.other_handle)));
    650468      // 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;
     469      var nHelp = (S.help || []).filter(function (h) { return h.actor_uri === uri; }).length;
    654470      var nFollow = FOLLOWS.filter(function (f) { return f.wardUri === uri; }).length;
    655471      if (nHelp) row.appendChild(el('span', 'tag help', '🛟 ' + nHelp));
     
    737553    card.appendChild(head);
    738554    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 || '';
    743555    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.
    746556      var d = document.createElement('details');
    747557      var sum = document.createElement('summary'); sum.textContent = p.cw; d.appendChild(sum);
    748       var inner = el('div'); inner.innerHTML = html; d.appendChild(inner);
     558      var inner = el('div'); inner.innerHTML = p.content || ''; d.appendChild(inner);
    749559      body.appendChild(d);
    750560    } else {
    751       body.innerHTML = html;   // server-sanitized HTML (same as Berichten)
     561      body.innerHTML = p.content || '';   // server-sanitized HTML (same as Berichten)
    752562    }
    753563    card.appendChild(body);
     
    890700
    891701  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(); });
     702  setInterval(refresh, 45000);   // live-ish while open
    907703  } catch (e) {
    908704    fatal((e && e.message) || String(e));
Note: See TracChangeset for help on using the changeset viewer.