Ignore:
File:
1 edited

Legend:

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

    rd56d471 r1ff801d  
    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; }
    5165
    5266  function helpCard(h) {
     
    7286      card.appendChild(a);
    7387    }
     88    card.appendChild(helpState(h));
    7489    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; });
    75164  }
    76165
     
    78167    var list = document.getElementById('help-list');
    79168    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     }
     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
    85200    var badge = document.getElementById('help-count');
    86     badge.textContent = help.length; badge.hidden = help.length === 0;
    87     show('help-empty', help.length === 0);
     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);
    88205  }
    89206
     
    351468  }
    352469
     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
    353530  function wardPanel(w) {
    354531    var uri = w.other_uri;
     
    358535    var set = el('div', 'g-panel-sec');
    359536    set.appendChild(el('h3', null, T.settings_title || 'Settings'));
    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     });
     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    }
    384558    panel.appendChild(set);
    385559
     
    420594      T.panel_follow_empty || '', followCard);
    421595
     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.
    422599    sectionInto(panel, T.panel_help || 'Calls for help',
    423       (S.help || []).filter(function (h) { return h.actor_uri === uri; }),
     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); }),
    424602      T.panel_help_empty || '', helpCard);
    425603
     
    453631    actRow.appendChild(rel);
    454632    act.appendChild(actRow);
    455     panel.appendChild(act);
     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);
    456638    return panel;
    457639  }
     
    467649      row.appendChild(el('span', 'who grow', handleOf(uri, w.other_handle)));
    468650      // Counts on the row: whatever is waiting must be visible with the panel shut.
    469       var nHelp = (S.help || []).filter(function (h) { return h.actor_uri === uri; }).length;
     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;
    470654      var nFollow = FOLLOWS.filter(function (f) { return f.wardUri === uri; }).length;
    471655      if (nHelp) row.appendChild(el('span', 'tag help', '🛟 ' + nHelp));
     
    553737    card.appendChild(head);
    554738    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 || '';
    555743    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.
    556746      var d = document.createElement('details');
    557747      var sum = document.createElement('summary'); sum.textContent = p.cw; d.appendChild(sum);
    558       var inner = el('div'); inner.innerHTML = p.content || ''; d.appendChild(inner);
     748      var inner = el('div'); inner.innerHTML = html; d.appendChild(inner);
    559749      body.appendChild(d);
    560750    } else {
    561       body.innerHTML = p.content || '';   // server-sanitized HTML (same as Berichten)
     751      body.innerHTML = html;   // server-sanitized HTML (same as Berichten)
    562752    }
    563753    card.appendChild(body);
     
    700890
    701891  renderAll(); pushState(); loadFeed(); loadFollowReqs();
    702   setInterval(refresh, 45000);   // live-ish while open
     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(); });
    703907  } catch (e) {
    704908    fatal((e && e.message) || String(e));
Note: See TracChangeset for help on using the changeset viewer.