Ignore:
Timestamp:
08/06/2026 01:56:08 AM (5 weeks ago)
Author:
Claude (agent) <aiclaude@…>
Branches:
main
Children:
0448e34
Parents:
04aca12
git-author:
Robin <roboburr@…> (08/06/2026 01:55:53 AM)
git-committer:
Claude (agent) <aiclaude@…> (08/06/2026 01:56:08 AM)
Message:

Berichten toont gesprekken: Berichten, Gesprekken en Verzonden vouwen samen

Berichten had zes chips, waarvan drie over hetzelfde gesprek gingen. Een
uitwisseling viel daardoor uit elkaar: wat jij stuurde stond onder Verzonden,
wat je terugkreeg onder Berichten of Gesprekken, en om een draad te volgen moest
je tussen chips heen en weer. Dat waren geen aparte pagina's maar client-side
filters over een enkele lijst, dus het probleem zat in het model, niet in de
weergave.

Antwoorden, mentions en je eigen verzonden berichten vouwen nu samen tot draden.
De sleutel: hangt het bericht aan een post van jou, dan is dat de draad --
ontvangen antwoorden kennen hun post via de join op posts, verzonden antwoorden
via ap_outbox.post_slug, en dat is dezelfde sleutel. Hangt het aan niets van jou
(een mention), dan loopt de draad per tegenpartij. De post wint van de persoon:
twee mensen onder dezelfde post voeren een gesprek, geen twee.

De context hoort bij de draad. Gaat het gesprek over een post, dan staat de link
ernaartoe in de kop, want zonder die context is een los antwoord in een lijst
niet te plaatsen. Binnen de draad leest het van oud naar nieuw, en jouw eigen
bijdragen staan aan de andere kant van de lijn zodat je de beurten ziet zonder
namen te lezen.

Likes, boosts, follows, reports en poll-uitslagen blijven losse regels: dat is
geen gesprek. De chips gaan van zes naar vier: Alles, Gesprekken, Activiteit,
Moderatie.

Twee dingen die daarbij moesten:

  • listOutbox selecteert post_slug (stond in de tabel, werd niet gelezen).
  • De filter kijkt naar ':scope > .msg-item', want de bubbels in een draad zijn geen zelfstandige regels meer; zoeken indexeert een draad op zijn hele tekst in plaats van op de eerste bubbel.

De titel van de post blijft leeg zolang hij onbekend is in plaats van terug te
vallen op de slug: het nieuwste bericht in een draad is vaak je eigen antwoord,
en dat kent alleen de slug -- zou die de titel worden, dan kon het ontvangen
antwoord eronder de echte titel niet meer invullen.

Getest tegen dev.klonkt.com: de groepering en de template gedraaid op de echte
database daar, en op een kopie ervan met een nagebouwd gesprek (twee ontvangen
antwoorden, jouw antwoord ertussen, plus een like). Dat levert een draad van
drie berichten op volgorde met de post als context, terwijl de like een losse
regel blijft. Suite 408/408.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r04aca12 r7875a80  
    639639      items.push({
    640640        type: 'sent', outboxId: m.id, to_handle: m.to_handle, in_reply_to: m.in_reply_to,
    641         content: m.content, editable: m.editable, language: m.language, created_at: m.created_at,
     641        post_slug: m.post_slug, content: m.content, editable: m.editable,
     642        language: m.language, created_at: m.created_at,
    642643      });
    643644    }
    644645  } catch { /* ignore */ }
     646  // Een verzonden antwoord kent zijn post_slug maar niet de titel (ap_outbox
     647  // bewaart die niet). Zonder titel toont een draad waarin JIJ als enige iets
     648  // zei alleen een slug, dus vullen we ze in één query aan.
     649  try {
     650    const missing = [...new Set(items.filter((i) => i.post_slug && !i.post_title).map((i) => i.post_slug))];
     651    if (missing.length) {
     652      const rows = db.prepare(
     653        `SELECT slug, title FROM posts WHERE slug IN (${missing.map(() => '?').join(',')})
     654           AND site_id = (SELECT id FROM sites WHERE slug = ?)`,
     655      ).all(...missing, slug);
     656      const byslug = new Map(rows.map((r) => [r.slug, r.title]));
     657      for (const i of items) if (i.post_slug && !i.post_title) i.post_title = byslug.get(i.post_slug) || null;
     658    }
     659  } catch { /* zonder titel valt de draad terug op de slug */ }
    645660  items.sort((a, b) => _msgTs(b) - _msgTs(a)); // NaN-safe (zie getNotifications)
    646661  const out = [];
     
    656671    out.push(it);
    657672  }
    658   return out.slice(off, off + lim);
     673  // Antwoorden, mentions en je eigen verzonden berichten vouwen samen tot
     674  // draden; likes/boosts/follows/reports blijven losse regels. Na deze stap
     675  // telt een draad als één item voor de paginering, wat klopt: je scrolt door
     676  // gesprekken, niet door losse zinnen.
     677  return groupConversations(out).slice(off, off + lim);
     678}
     679
     680// De drie soorten die samen een gesprek vormen. Vroeger zaten ze in drie
     681// aparte chips: 'reply' en 'mention' onder Berichten/Gesprekken (afhankelijk van
     682// de zichtbaarheid) en 'sent' onder Verzonden. Wie een uitwisseling wilde volgen
     683// moest dus tussen chips heen en weer, terwijl het één draad is.
     684const CONV_TYPES = new Set(['reply', 'mention', 'sent']);
     685
     686/** Waar hangt dit bericht aan? Twee soorten draden, en de volgorde telt:
     687 *
     688 *  1. Aan een post van jou. Een ontvangen antwoord kent zijn post via de join
     689 *     op `posts`, een verzonden antwoord via ap_outbox.post_slug. Dat is
     690 *     dezelfde sleutel, en daarom staan ze nu in dezelfde draad.
     691 *  2. Aan een persoon. Een mention hangt aan niets van jou (het is iemands
     692 *     eigen post waarin je genoemd wordt) en heeft geen post_slug; die draad
     693 *     loopt per tegenpartij.
     694 *
     695 *  De post wint van de persoon: twee mensen die onder dezelfde post reageren
     696 *  voeren één gesprek, geen twee. Geeft null terug voor alles wat geen gesprek
     697 *  is (likes, boosts, follows, reports, poll-uitslagen); die stromen ongemoeid
     698 *  door.
     699 */
     700export function threadKey(it) {
     701  if (!it || !CONV_TYPES.has(it.type)) return null;
     702  if (it.post_slug) return `post:${it.post_slug}`;
     703  const who = it.handle || it.to_handle || '';
     704  const norm = String(who).trim().toLowerCase().replace(/^@/, '');
     705  return norm ? `actor:${norm}` : null;
     706}
     707
     708/** Vouw losse berichten samen tot draden, met alles wat geen gesprek is
     709 *  ongemoeid ertussen. Verwacht [items] al gesorteerd op created_at aflopend
     710 *  (zoals getMessages ze aanlevert); de draad komt daardoor op de plek van zijn
     711 *  nieuwste bericht te staan en `created_at` van de draad IS dat bericht. Binnen
     712 *  de draad draait het om: een gesprek leest naar beneden, oud naar nieuw.
     713 */
     714export function groupConversations(items) {
     715  const threads = new Map();
     716  const out = [];
     717  for (const it of items || []) {
     718    const key = threadKey(it);
     719    if (!key) { out.push(it); continue; }
     720    let t = threads.get(key);
     721    if (!t) {
     722      // Eerste keer dat we deze draad zien = het nieuwste bericht erin, want de
     723      // invoer is aflopend gesorteerd. Vandaar created_at hier en niet later.
     724      t = { type: 'thread', key, post: null, people: [], messages: [], created_at: it.created_at };
     725      threads.set(key, t);
     726      out.push(t);
     727    }
     728    t.messages.push(it);
     729    // De context bij de draad: gaat het over een post, dan hoort de link
     730    // erbij, anders is een los antwoord in een lijst niet te plaatsen.
     731    // De titel blijft LEEG zolang hij onbekend is, in plaats van terug te
     732    // vallen op de slug: het nieuwste bericht in een draad is vaak je eigen
     733    // verzonden antwoord, en dat kent alleen de slug. Zou die de titel worden,
     734    // dan kan het ontvangen antwoord eronder de echte titel niet meer
     735    // invullen. De terugval op de slug hoort in de weergave, niet in de data.
     736    if (it.post_slug) {
     737      if (!t.post) t.post = { slug: it.post_slug, title: it.post_title || null };
     738      else if (!t.post.title && it.post_title) t.post.title = it.post_title;
     739    }
     740  }
     741  for (const t of threads.values()) {
     742    t.messages.sort((a, b) => _msgTs(a) - _msgTs(b));
     743    t.count = t.messages.length;
     744    // Wie zit er in dit gesprek, jij niet meegerekend: 'sent' ben jij.
     745    const seen = new Set();
     746    for (const m of t.messages) {
     747      if (m.type === 'sent') continue;
     748      const h = m.handle || m.name;
     749      if (!h || seen.has(h)) continue;
     750      seen.add(h);
     751      t.people.push({ name: m.name, handle: m.handle, icon: m.icon, url: m.url });
     752    }
     753    // Heb JIJ in deze draad iets gezegd? Bepaalt of hij als uitwisseling of als
     754    // onbeantwoord bericht leest.
     755    t.mine = t.messages.some((m) => m.type === 'sent');
     756  }
     757  return out;
    659758}
    660759
     
    29073006}
    29083007export function listOutbox(siteSlug) {
    2909   return db.prepare('SELECT id, content, to_handle, in_reply_to, language, created_at FROM ap_outbox WHERE site_slug = ? ORDER BY created_at DESC')
     3008  // post_slug reist mee sinds Berichten gesprekken toont: het is de sleutel
     3009  // waarop een verzonden antwoord bij de ontvangen antwoorden op dezelfde post
     3010  // gaat staan (zie threadKey). Zonder die kolom viel een uitwisseling uit
     3011  // elkaar in "Verzonden" en "Gesprekken".
     3012  return db.prepare('SELECT id, content, to_handle, to_actor, post_slug, in_reply_to, language, created_at FROM ap_outbox WHERE site_slug = ? ORDER BY created_at DESC')
    29103013    .all(siteSlug).map((r) => { const c = stripLeadingMentions(r.content); return { ...r, content: c, editable: outboxEditableText(c) }; });
    29113014}
Note: See TracChangeset for help on using the changeset viewer.