Changeset 9eb6e1b in Klonkt for src/assets/js/mod


Ignore:
Timestamp:
09/30/2026 06:41:45 AM (4 hours ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
86cfa1a
Parents:
3ceaef6
Message:

Een fediverse-post ziet eruit als een post, ook in de zoekbalk

Plak je een link in de zoekbalk, dan laadt de uitklaplijst nu de post zelf
in plaats van te zoeken naar de tekst van de link. Dezelfde kaart staat op de
zoekpagina en op /authorize_interaction, dus één render op drie plekken.

DE KAART. Wat eromheen stond is teruggebracht tot een kop met gezicht, de
inhoud, en één rij knoppen: Reageer, Like, Boost en het origineel. Weg zijn de
zin "je reageert op", de aparte knop naar het origineel, de twee grote pillen,
"of reageer:" en het antwoordvenster dat altijd openstond. Antwoorden klapt
open als je erom vraagt; melden staat onderaan en niet in de weg.

Foto's staan in een raster met vaste rijhoogtes. Met verhoudingen groeide het
mee met de breedte, en werd op een desktop de eerste van drie foto's zo hoog
dat de knoppen onder de vouw van de uitklaplijst verdwenen.

EEN INHOUDSWAARSCHUWING WORDT GERESPECTEERD. De oude render toonde alles open,
ook wat de schrijver achter een waarschuwing had gezet.

MOBIEL. Elke knop minstens 44 punten. Op een smal scherm vallen de woorden weg
en blijven de tekens staan, gelijk verdeeld; de woorden blijven bestaan voor
wie het scherm voorgelezen krijgt. Nagemeten op 375 pixels: geen horizontale
overloop, kleinste tikdoel 44, de knoppen in beeld zonder te scrollen.

DE PREVIEW IS COMPACT. Waarderen en boosten kan ter plekke; antwoorden en
melden gaan naar de volle pagina, met ?reply=1 of ?report=1 zodat dat deel
daar al openstaat. Een editor in een uitklaplijst is op een telefoon geen plek
om te schrijven. Zonder JS doet dezelfde link hetzelfde.

/search/remote levert het fragment, achter dezelfde grens als de volle
pagina: alleen wie de site beheert laat ophalen. Die grens weegt hier zwaarder,
want deze route wordt bij elke toetsaanslag geraakt; zonder hem is hij een
haalservice die iedereen deze server op adressen naar keuze laat afsturen.
Voor een bezoeker 204, en dan valt de zoekbalk terug op de gewone suggesties.
Elke toetsaanslag krijgt een volgnummer, zodat een traag antwoord op een oude
vraag niet overschrijft wat je inmiddels hebt getypt.

Waarderen en boosten ter plekke verhuisde van authorize-interaction.js naar
chrome.js: de kaart staat nu op elke pagina, via de zoekbalk. De zoekpagina
stuurde je bij een like eerder weg naar /authorize_interaction.

Onderweg: /authorize_interaction haalt nu ook ondertekend op, net als de
zoekbalk. Anders zegt die pagina "niet gevonden" over een post voor volgers die
de preview wel liet zien.

Getoetst: de preview met de post, ondertekend; wat er in de compacte kaart wel
en niet staat; dat een bezoeker via de preview niets laat ophalen; dat een
zoekterm niets ophaalt; de waarschuwing; het venster op verzoek. Tegenproef:
rechtencheck eruit, waarschuwing genegeerd, compact genegeerd en anoniem
ophalen laten elk hun eigen toets omvallen. Volledige suite 1283 groen.

Het gedrag in chrome.js is in de browser nagelopen met een nagebootste
server: herkennen, laden, invoegen, de volgorde bij een traag antwoord, de
terugval bij 204, waarderen en boosten heen en terug, en het antwoordvenster
open en dicht met de focus op de goede plek.

shaer-utpi

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

Location:
src/assets/js/mod
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • src/assets/js/mod/authorize-interaction.js

    r3ceaef6 r9eb6e1b  
    4141// ── volgend blok ──
    4242
    43 /* Like/Boost toggle in place — POST via fetch, flip the button, stay on the page. */
    44 (function(){
    45   if (window.__fediReactWired) return; window.__fediReactWired = true;
    46   document.addEventListener('submit', function(e){
    47     var f = e.target.closest && e.target.closest('.fedi-react-form');
    48     if (!f) return;
    49     e.preventDefault();
    50     var btn = f.querySelector('button'); if (!btn || btn.disabled) return;
    51     btn.disabled = true;
    52     var body = new URLSearchParams();
    53     new FormData(f).forEach(function(v, k){ body.append(k, v); });
    54     fetch(f.action, { method: 'POST', body: body, headers: { 'X-Requested-With': 'fetch' }, credentials: 'same-origin' })
    55       .then(function(r){ return r.ok ? r.json() : null; })
    56       .then(function(j){
    57         if (j) {
    58           var on = !!j.on;
    59           btn.classList.toggle('is-on', on);
    60           var lbl = btn.querySelector('.fedi-bigact-label');
    61           if (lbl) lbl.textContent = on ? (btn.getAttribute('data-on') || lbl.textContent) : (btn.getAttribute('data-off') || lbl.textContent);
    62         }
    63       })
    64       .catch(function(){})
    65       .then(function(){ btn.disabled = false; });
    66   });
    67 })();
     43/* Waarderen en boosten ter plekke staat sinds de herbouw van de kaart in
     44   chrome.js: de kaart staat nu ook in de zoekbalk, op elke pagina. */
    6845}
  • src/assets/js/mod/chrome.js

    r3ceaef6 r9eb6e1b  
    161161    box.innerHTML = html;
    162162  }
    163   var _sTimer;
     163  // Een ADRES is geen zoekterm (shaer-utpi). Dezelfde regel als lookupUri in
     164  // routes/search.js: een schema is verplicht, anders wordt zoeken naar
     165  // "soundfabrics.nl" een netwerkverzoek. De server beslist daarna nog eens,
     166  // met de rechten erbij; dit scheelt alleen een vraag die toch nee oplevert.
     167  function lookupUri(q) {
     168    if (!/^https?:\/\//i.test(q) || /\s/.test(q)) return null;
     169    try { var u = new URL(q); return u.hostname.indexOf('.') < 0 ? null : u.href; } catch (e) { return null; }
     170  }
     171  function allLink(ov, q) {
     172    var action = (ov && ov.getAttribute('data-action')) || '/search';
     173    return '<a class="ss-all" href="' + esc(action) + '?q=' + encodeURIComponent(q) + '">' + esc(_st().all) + '</a>';
     174  }
     175  function fetchSuggest(box, q, ov, seq) {
     176    var url = (ov && ov.getAttribute('data-suggest')) || '/search/suggest';
     177    fetch(url + '?q=' + encodeURIComponent(q))
     178      .then(function(r){ return r.ok ? r.json() : null; })
     179      .then(function(d){ if (d && seq === _sSeq) renderSuggest(box, d, q, ov); })
     180      .catch(function(){});
     181  }
     182  // Elke toetsaanslag krijgt een volgnummer. Een ophaling van een andere
     183  // server duurt soms seconden; zonder nummer overschrijft een traag antwoord
     184  // op een oude vraag wat je inmiddels hebt getypt.
     185  var _sTimer, _sSeq = 0;
    164186  document.addEventListener('input', function(e) {
    165187    var inp = e.target.closest && e.target.closest('#search-overlay input[name="q"]');
    … …  
    168190    if (!box) return;
    169191    var q = inp.value.trim();
     192    var seq = ++_sSeq;
    170193    clearTimeout(_sTimer);
    171194    if (q.length < 2) { box.innerHTML = ''; return; }
     195    var ov = document.getElementById('search-overlay');
     196    var address = lookupUri(q);
     197    if (!address) { _sTimer = setTimeout(function() { fetchSuggest(box, q, ov, seq); }, 200); return; }
     198    // Een geplakt adres: de post zelf, niet de tekst van de link doorzocht.
     199    box.innerHTML = '<div class="ss-remote-loading">' + esc(_st().looking) + '</div>';
    172200    _sTimer = setTimeout(function() {
    173       var ov = document.getElementById('search-overlay');
    174       var url = (ov && ov.getAttribute('data-suggest')) || '/search/suggest';
    175       fetch(url + '?q=' + encodeURIComponent(q))
    176         .then(function(r){ return r.ok ? r.json() : null; })
    177         .then(function(d){ if (d) renderSuggest(box, d, q, ov); })
    178         .catch(function(){});
    179     }, 200);
    180   });
    181   // Click on a suggestion → close the overlay (the link/boost handles navigation).
     201      var url = (ov && ov.getAttribute('data-remote')) || '/search/remote';
     202      fetch(url + '?q=' + encodeURIComponent(address), { credentials: 'same-origin', headers: { 'X-Requested-With': 'fetch' } })
     203        .then(function(r) {
     204          if (seq !== _sSeq) return;
     205          // 204: geen recht om op te halen (of toch geen adres). Dan is het
     206          // gewoon een zoekterm en krijg je de gewone suggesties.
     207          if (r.status === 204 || !r.ok) { fetchSuggest(box, q, ov, seq); return; }
     208          return r.text().then(function(html) { if (seq === _sSeq) box.innerHTML = html + allLink(ov, q); });
     209        })
     210        .catch(function() { if (seq === _sSeq) fetchSuggest(box, q, ov, seq); });
     211    }, 300);
     212  });
     213  // Een klik op een suggestie sluit de overlay, want de link gaat ergens heen.
     214  // Behalve wat in een nieuw tabblad opent (het origineel, een profiel): dan
     215  // blijf je hier, en hoort de kaart er nog te staan als je terugkomt.
    182216  document.addEventListener('click', function(e) {
    183     if (e.target.closest && e.target.closest('#search-suggest a')) { var o = overlay(); if (o) o.hidden = true; }
     217    var a = e.target.closest && e.target.closest('#search-suggest a');
     218    if (a && a.getAttribute('target') !== '_blank') { var o = overlay(); if (o) o.hidden = true; }
     219  });
     220})();
     221
     222// ── De kaart van een post van een andere server ─────────────────────────
     223// Staat hier en niet in authorize-interaction.js, want de kaart staat nu op
     224// drie plekken: de live-preview (elke pagina, via de zoekbalk), de zoekpagina
     225// en /authorize_interaction. Alles luistert op document, dus een kaart die
     226// later binnenkomt (de preview, een htmx-wissel) doet vanzelf mee.
     227(function() {
     228  if (window.__remoteCardWired) return; window.__remoteCardWired = true;
     229
     230  // Waarderen en boosten ter plekke: POST via fetch, de knop omzetten, blijven
     231  // waar je bent. Zonder JS doet het formulier hetzelfde met een omweg.
     232  document.addEventListener('submit', function(e) {
     233    var f = e.target.closest && e.target.closest('.fedi-react-form');
     234    if (!f) return;
     235    e.preventDefault();
     236    var btn = f.querySelector('button'); if (!btn || btn.disabled) return;
     237    btn.disabled = true;
     238    var body = new URLSearchParams();
     239    new FormData(f).forEach(function(v, k) { body.append(k, v); });
     240    fetch(f.action, { method: 'POST', body: body, headers: { 'X-Requested-With': 'fetch' }, credentials: 'same-origin' })
     241      .then(function(r) { return r.ok ? r.json() : null; })
     242      .then(function(j) {
     243        if (!j) return;
     244        var on = !!j.on;
     245        btn.classList.toggle('is-on', on);
     246        btn.setAttribute('aria-pressed', on ? 'true' : 'false');
     247        var lbl = btn.querySelector('.rp-act-label');
     248        if (lbl) lbl.textContent = on ? (btn.getAttribute('data-on') || lbl.textContent) : (btn.getAttribute('data-off') || lbl.textContent);
     249      })
     250      .catch(function() {})
     251      .then(function() { btn.disabled = false; });
     252  });
     253
     254  // Antwoorden klapt open waar de kaart staat. Staat er geen venster (de
     255  // preview), dan volgt de knop gewoon zijn link naar de volle pagina, met het
     256  // venster daar al open.
     257  document.addEventListener('click', function(e) {
     258    var open = e.target.closest && e.target.closest('[data-rp-reply]');
     259    var close = !open && e.target.closest && e.target.closest('[data-rp-reply-close]');
     260    if (!open && !close) return;
     261    var panel = document.getElementById('rp-reply');
     262    if (!panel) return;
     263    e.preventDefault();
     264    var btn = document.querySelector('[data-rp-reply][aria-controls="rp-reply"]');
     265    var show = !!open && panel.hidden;
     266    panel.hidden = !show;
     267    if (btn) btn.setAttribute('aria-expanded', show ? 'true' : 'false');
     268    if (show) {
     269      var ed = panel.querySelector('.re-editor, textarea');
     270      if (ed) ed.focus();
     271      // Op een telefoon staat het venster vaak onder de vouw.
     272      if (panel.scrollIntoView) panel.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
     273    } else if (btn) {
     274      btn.focus();
     275    }
    184276  });
    185277})();
Note: See TracChangeset for help on using the changeset viewer.