Changeset 2086ea2 in Klonkt for src/assets/js/ward.js


Ignore:
Timestamp:
08/12/2026 09:16:09 PM (3 months ago)
Author:
Robin <roboburr@…>
Children:
39dab6b
Parents:
5412cbb
Message:

Ward-PWA: de composer met media

Schrijven met foto, geluid of video, in de browser.

DEZELFDE POORT ALS DE APP, en dat is de kern. De composer bouwt precies
de AS2 Create die Shaer over C2S stuurt en geeft die aan
ingestOutboxActivity: die doet de compose-poort (par. 5), de
attachment-validatie en de zichtbaarheid. GEEN eigen posting-pad, want
dan zou de web-ward een sluiproute om de guardians heen zijn. Een test
bewaakt precies dat: een gated ward wordt ook via de browser
tegengehouden.

Media gaan eerst apart naar /ward/api/upload, in DEZELFDE bak als de
app (reply-media), zodat een foto van een app-ward en een browser-ward
niet van twee plekken federeert. De server maakt de rest af zoals altijd:
posterframe voor video, waveform voor audio. De post draagt daarna alleen
de url, dus de payload blijft klein.

Sessie-geauthenticeerd, niet met een bearer: /ward draait op je eigen
ingelogde Klonkt, dus de browser heeft geen token nodig zoals de app.

New file: (geen)
Changed files:
src/routes/ward.js

  • /api/upload: sessie-geauthenticeerd, zelfde filters/map als C2S
  • /api/post: bouwt de AS2 Create, door ingestOutboxActivity

src/views/pages/ward.ejs

  • schrijfknop, composer-dialoog met tekst, media en zichtbaarheid

src/assets/js/ward.js

  • upload-per-bestand, media-chips, plaatsen; opruiming van een dubbel geraakte doc-comment

src/assets/css/ward.css

  • composer, media-chips, tools

src/services/i18n.js

  • twaalf sleutels in nl, en, de

test/ward-pwa.test.js

  • de gate geldt ook in de browser; een vrij account plaatst met media; leeg wordt geweigerd; een vreemde kan niet uploaden

remarks: 903 tests groen. Opnemen van audio/video in de browser zelf
(MediaRecorder) is nog niet gebouwd; nu kies je een bestand. Bead volgt.

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

File:
1 edited

Legend:

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

    r5412cbb r2086ea2  
    423423}
    424424
    425 /** De controle kost per guardian een remote fetch, dus hij komt ná het tekenen. *//** De controle kost per guardian een remote fetch, dus hij komt ná het tekenen. */
     425/**
     426 * De composer: schrijven met foto, geluid of video.
     427 *
     428 * De media gaan eerst apart naar /ward/api/upload (dezelfde bak als de app),
     429 * en de post draagt daarna alleen de url mee. Zo blijft de post-payload klein
     430 * en kan de server elke upload los valideren en verrijken (poster, waveform).
     431 */
     432function wireComposer() {
     433  const dlg = document.getElementById('compose-dialog');
     434  const openBtn = document.getElementById('compose-open');
     435  const text = document.getElementById('compose-text');
     436  const mediaBox = document.getElementById('compose-media');
     437  const status = document.getElementById('compose-status');
     438  const sendBtn = document.getElementById('compose-send');
     439  let media = [];   // { url, mediaType, name }
     440  let uploading = false;
     441
     442  function reset() {
     443    text.value = '';
     444    media = [];
     445    mediaBox.replaceChildren();
     446    status.textContent = '';
     447    updateSend();
     448  }
     449  function updateSend() {
     450    // Een lege post kan niet: tekst OF media, en nooit tijdens een upload.
     451    sendBtn.disabled = (!text.value.trim() && !media.length) || uploading;
     452  }
     453  function renderMedia() {
     454    mediaBox.replaceChildren();
     455    media.forEach((m, i) => {
     456      const chip = el('div', 'media-chip');
     457      if (m.mediaType.startsWith('image/')) {
     458        const img = el('img'); img.src = m.url; img.alt = m.name || ''; chip.append(img);
     459      } else {
     460        chip.append(el('span', 'kind', m.mediaType.startsWith('audio/') ? '🎤' : '🎬'));
     461        chip.append(el('span', 'name', m.name || ''));
     462      }
     463      const x = el('button', 'drop', '✕'); x.type = 'button';
     464      x.addEventListener('click', () => { media.splice(i, 1); renderMedia(); updateSend(); });
     465      chip.append(x);
     466      mediaBox.append(chip);
     467    });
     468  }
     469
     470  async function upload(file) {
     471    if (media.length >= 4) return;
     472    // 32 MB is de servergrens; hier alvast vangen scheelt een mislukte upload.
     473    if (file.size > 32 * 1024 * 1024) { status.textContent = state.strings.tooBig; return; }
     474    uploading = true; status.textContent = state.strings.uploading; updateSend();
     475    try {
     476      const fd = new FormData();
     477      fd.append('file', file);
     478      const r = await fetch('/ward/api/upload', { method: 'POST', body: fd });
     479      if (r.ok) { media.push(await r.json()); renderMedia(); status.textContent = ''; }
     480      else status.textContent = (await r.json().catch(() => ({}))).error === 'media_type'
     481        ? 'Dat bestandstype kan niet.' : 'Uploaden lukte niet.';
     482    } catch { status.textContent = 'Uploaden lukte niet.'; }
     483    uploading = false; updateSend();
     484  }
     485
     486  openBtn.addEventListener('click', () => { reset(); dlg.showModal(); });
     487  document.getElementById('compose-cancel').addEventListener('click', () => dlg.close());
     488  text.addEventListener('input', updateSend);
     489
     490  dlg.querySelectorAll('input[type=file]').forEach((inp) => {
     491    inp.addEventListener('change', () => {
     492      if (inp.files[0]) upload(inp.files[0]);
     493      inp.value = '';   // zelfde bestand opnieuw kunnen kiezen
     494    });
     495  });
     496
     497  sendBtn.addEventListener('click', async () => {
     498    if (sendBtn.disabled) return;
     499    sendBtn.disabled = true;
     500    try {
     501      const r = await fetch('/ward/api/post', {
     502        method: 'POST',
     503        headers: { 'Content-Type': 'application/json' },
     504        body: JSON.stringify({
     505          text: text.value,
     506          media,
     507          visibility: document.getElementById('compose-vis').value,
     508        }),
     509      });
     510      if (r.ok) { dlg.close(); loadFeed(); }
     511      else status.textContent = 'Plaatsen lukte niet.';
     512    } catch { status.textContent = 'Plaatsen lukte niet.'; }
     513    updateSend();
     514  });
     515}
     516
     517/** De controle kost per guardian een remote fetch, dus hij komt ná het tekenen. */
    426518async function loadChecks() {
    427519  if (!state.guardians.length) return;
    … …  
    449541wireBuoy();
    450542wireSky();
     543wireComposer();
    451544loadFeed();
    452545renderOffers();
Note: See TracChangeset for help on using the changeset viewer.