Changeset 5218863 in Klonkt for src/assets/js/ward.js


Ignore:
Timestamp:
08/12/2026 08:25:10 PM (3 months ago)
Author:
Robin <roboburr@…>
Children:
4bb93aa
Parents:
084b046
Message:

De Ward PWA: het gesprek over de hemel, en de zwaai-hand

Plak 4. Een tik op een gezicht opent nu echt iets.

HET GESPREK is een overlay OVER de levende hemel, geen aparte pagina:
de hemel eronder blijft dezelfde draaiende instantie, en het paneel is
doorschijnend met blur, zoals de glazen bubbels in de app. Dezelfde
bron als de C2S-lezing die Shaer gebruikt (conversationHistory):
inkomend de ap_mentions, uitgaand de eigen directe notes. Openen is
lezen: markRead neemt het maximum, dus een oud toestel kan nooit iets
weer ongelezen maken. Verversen is pollen om de acht seconden zolang
het gesprek open staat; het ritme van een gesprek is geen milliseconde,
en een socket kan later zonder iets te slopen.

DE ZWAAI-HAND hangt in dezelfde hoek als de boei en ze wisselen elkaar
af met de stand: met de kaarten verdwijnt de boei, op de hemel hangt de
hand. Slepen op een gezicht opent het paneel met de drie antwoorden, en
de tekst die je kiest IS het bericht. De treffer is sky.pick(): exact
dezelfde rekensom als een tik, dus mikken gedraagt zich overal gelijk.

Changed files:
src/routes/ward.js

  • /api/thread: beide kanten, cursor, markRead bij openen
  • /api/message: direct bericht of zwaai (shaer:wave reist mee)

src/assets/js/ward-sky.js

  • pick() als publieke mikfunctie

src/assets/js/ward.js

  • thread-overlay met poll, verstuurvak, Escape sluit
  • de hand: slepen, mikken, paneel, versturen

src/views/pages/ward.ejs

  • thread, hand en zwaai-paneel in de schil

src/assets/css/ward.css

  • doorschijnend gesprek, bubbels, hand, paneel

src/services/i18n.js

  • tien sleutels in nl, en, de

test/ward-pwa.test.js

  • het gesprek draagt beide kanten en de zwaai-vlag overleeft; versturen eist een echte ontvanger en echte tekst

remarks: 897 tests groen. Nog niet geport: de (+) voor een nieuw
gesprek, Friends/Orbit-panelen, de composer met media, de mini-speler
en de themakleur; beads volgen. Slepen met een echte vinger is aan
Robin op dev.

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

File:
1 edited

Legend:

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

    r084b046 r5218863  
    231231  const sky = new SkyRenderer(canvas, (i) => {
    232232    const c = convs[i];
    233     if (c) window.location.href = `/ward/thread?with=${encodeURIComponent(c.uri)}`;
     233    if (c) openThread(c);
     234  });
     235
     236  /* ── Het gesprek: een overlay OVER de levende hemel ────────────────────
     237     Geen aparte pagina: de hemel eronder blijft dezelfde draaiende
     238     instantie, precies zoals de thread in Shaer over de hemel schuift. */
     239  const thread = document.getElementById('thread');
     240  const list = document.getElementById('thread-list');
     241  let openWith = null;
     242  let pollTimer = null;
     243
     244  async function loadThread() {
     245    if (!openWith) return;
     246    try {
     247      const r = await fetch(`/ward/api/thread?with=${encodeURIComponent(openWith.uri)}`, { headers: { Accept: 'application/json' } });
     248      if (!r.ok) return;
     249      const { items } = await r.json();
     250      list.replaceChildren();
     251      // De server geeft nieuwste eerst; een gesprek lees je oudste eerst.
     252      [...items].reverse().forEach((m) => {
     253        const b = el('div', `bubble ${m.dir}${m.wave ? ' wave' : ''}`);
     254        const body = el('div', 'body');
     255        body.innerHTML = m.bodyHtml || '';
     256        b.append(body, el('span', 'when', m.whenText || ''));
     257        list.append(b);
     258      });
     259      list.scrollTop = list.scrollHeight;
     260    } catch { /* volgende poll probeert opnieuw */ }
     261  }
     262
     263  function openThread(c) {
     264    openWith = c;
     265    document.getElementById('thread-name').textContent = c.name;
     266    thread.hidden = false;
     267    loadThread();
     268    // Pollen zolang het gesprek open staat: eenvoudig en zuinig genoeg.
     269    // Een socket kan later; het ritme van een gesprek is geen milliseconde.
     270    clearInterval(pollTimer);
     271    pollTimer = setInterval(loadThread, 8000);
     272  }
     273
     274  function closeThread() {
     275    openWith = null;
     276    thread.hidden = true;
     277    clearInterval(pollTimer);
     278  }
     279  document.getElementById('thread-close').addEventListener('click', closeThread);
     280  document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && openWith) closeThread(); });
     281
     282  document.getElementById('thread-form').addEventListener('submit', async (e) => {
     283    e.preventDefault();
     284    const input = document.getElementById('thread-text');
     285    const text = input.value.trim();
     286    if (!text || !openWith) return;
     287    input.value = '';
     288    await sendMessage(openWith.uri, text, false);
     289    loadThread();
     290  });
     291
     292  async function sendMessage(to, text, wave) {
     293    try {
     294      const r = await fetch('/ward/api/message', {
     295        method: 'POST',
     296        headers: { 'Content-Type': 'application/json' },
     297        body: JSON.stringify({ to, text, wave }),
     298      });
     299      return r.ok;
     300    } catch { return false; }
     301  }
     302
     303  /* ── De zwaai-hand (§5) ────────────────────────────────────────────────
     304     Dezelfde hoek als de boei, en ze wisselen elkaar af met de stand: met de
     305     kaarten verdwijnt de boei, op de hemel hangt de hand. Slepen op een
     306     gezicht opent het paneel; de tekst die je kiest IS het bericht. */
     307  const hand = document.getElementById('hand');
     308  const waveDlg = document.getElementById('wave-dialog');
     309  let waveTarget = null;
     310
     311  (() => {
     312    let dragging = false, dx = 0, dy = 0;
     313    hand.addEventListener('pointerdown', (e) => {
     314      dragging = true;
     315      hand.setPointerCapture(e.pointerId);
     316      const r = hand.getBoundingClientRect();
     317      dx = e.clientX - r.left; dy = e.clientY - r.top;
     318      hand.classList.add('dragging');
     319    });
     320    hand.addEventListener('pointermove', (e) => {
     321      if (!dragging) return;
     322      hand.style.left = `${e.clientX - dx}px`;
     323      hand.style.top = `${e.clientY - dy}px`;
     324      hand.style.right = 'auto'; hand.style.bottom = 'auto';
     325    });
     326    const drop = (e) => {
     327      if (!dragging) return;
     328      dragging = false;
     329      hand.classList.remove('dragging');
     330      hand.style.left = hand.style.top = 'auto';
     331      hand.style.right = ''; hand.style.bottom = '';
     332      // Zelfde rekensom als een tik op de hemel: het dichtstbijzijnde gezicht
     333      // binnen bereik wint, en ernaast is bewust niets.
     334      const r = canvas.getBoundingClientRect();
     335      const hit = sky.pick(e.clientX - r.left, e.clientY - r.top);
     336      if (hit != null && convs[hit]) {
     337        waveTarget = convs[hit];
     338        document.getElementById('wave-title').textContent =
     339          state.strings.waveTo.replace('{who}', waveTarget.name);
     340        waveDlg.showModal();
     341      }
     342    };
     343    hand.addEventListener('pointerup', drop);
     344    hand.addEventListener('pointercancel', drop);
     345  })();
     346
     347  waveDlg.addEventListener('click', async (e) => {
     348    const btn = e.target.closest('button');
     349    if (!btn) return;
     350    waveDlg.close();
     351    if (btn.id === 'wave-cancel' || !waveTarget) return;
     352    const isWave = btn.dataset.wave === '1';
     353    const text = isWave ? '👋' : btn.dataset.text;
     354    if (await sendMessage(waveTarget.uri, text, isWave)) openThread(waveTarget);
    234355  });
    235356
    … …  
    244365    canvas.style.pointerEvents = on ? 'auto' : 'none';
    245366    document.body.classList.toggle('messages-mode', on);
     367    document.getElementById('buoy').hidden = on;
     368    hand.hidden = !on;
     369    if (!on) closeThread();
    246370    btn.textContent = on ? state.strings.toCards : state.strings.toSky;
    247371    btn.setAttribute('aria-pressed', String(on));
Note: See TracChangeset for help on using the changeset viewer.