source: Klonkt/test/messages.test.js

main
Last change on this file was 7875a80, checked in by Claude (agent) <aiclaude@…>, 5 weeks ago

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@…>

  • Property mode set to 100644
File size: 6.1 KB
Line 
1// Messages-centrum: getMessages voegt notificaties + eigen outbound replies samen
2// in één stroom, groepeert opeenvolgende likes/boosts op dezelfde post, en geeft
3// visibility door (voor de privé-badge). Besluit Robin+Bart 2026-07-16.
4//
5// Sinds Berichten gesprekken toont vouwen antwoorden, mentions en je eigen
6// verzonden berichten samen tot draden (zie messages-conversations.test.js voor
7// de groepeerlogica zelf); likes, boosts en follows blijven losse regels.
8//
9// Run: npm test (= node --test)
10
11import { test } from 'node:test';
12import assert from 'node:assert/strict';
13
14process.env.DATABASE_PATH = ':memory:';
15process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
16
17const dbMod = await import('../src/config/database.js');
18const db = dbMod.default;
19const AP = await import('../src/services/ActivityPubService.js');
20
21dbMod.initializeDatabase();
22
23// ── Seed: site + post + interacties + eigen reply ────────────────────────
24db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
25 .run('u1', 'u1', 'u1@test', 'x', 'god');
26db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
27db.prepare(`INSERT INTO posts (id, site_id, slug, author_id, title, content, status, type, created_at, updated_at, published_at)
28 VALUES ('p1','s1','mijn-post','u1','Mijn post','<p>x</p>','published','post',datetime('now'),datetime('now'),datetime('now'))`).run();
29
30function addInter(kind, actor, createdAt, extra = {}) {
31 db.prepare(`INSERT INTO ap_interactions (kind, post_id, object_uri, actor_uri, actor_name, actor_icon, content, visibility, created_at)
32 VALUES (?,?,?,?,?,?,?,?,?)`)
33 .run(kind, 'p1', extra.object_uri || '', 'https://r.test/u/' + actor, actor, extra.icon || null,
34 extra.content || null, extra.visibility || 'public', createdAt);
35}
36// drie likes kort na elkaar (zelfde post) → moeten groeperen tot één item
37addInter('like', 'Anna', '2026-07-18 10:00:00');
38addInter('like', 'Ben', '2026-07-18 10:01:00');
39addInter('like', 'Cas', '2026-07-18 10:02:00');
40// een privé-reply (direct) ertussen, later
41addInter('reply', 'Dana', '2026-07-18 11:00:00', { object_uri: 'https://r.test/n/1', content: '<p>psst geheim</p>', visibility: 'direct' });
42// eigen outbound reply, nog later
43db.prepare(`INSERT INTO ap_outbox (id, site_slug, post_id, post_slug, in_reply_to, to_actor, to_handle, content, created_at)
44 VALUES ('out1','me','p1','mijn-post','https://r.test/n/1','https://r.test/u/Dana','@dana@r.test','<p>mijn antwoord</p>','2026-07-18 12:00:00')`).run();
45
46const msgs = AP.getMessages('me', 50);
47
48test('eigen outbound reply en het ontvangen antwoord staan in EEN draad', () => {
49 const thread = msgs.find((m) => m.type === 'thread');
50 assert.ok(thread, 'draad ontbreekt');
51 assert.equal(msgs[0], thread, 'de draad met het nieuwste bericht hoort bovenaan');
52 const sent = thread.messages.find((m) => m.type === 'sent');
53 assert.ok(sent, 'sent-item ontbreekt');
54 assert.equal(sent.outboxId, 'out1');
55 assert.equal(sent.to_handle, '@dana@r.test');
56 // Hier gaat het om: Dana's antwoord en het jouwe zaten in aparte chips
57 // (Gesprekken en Verzonden) en staan nu op volgorde in dezelfde draad.
58 assert.deepEqual(thread.messages.map((m) => m.type), ['reply', 'sent']);
59 assert.equal(thread.post.slug, 'mijn-post');
60});
61
62test('opeenvolgende likes op dezelfde post groeperen tot één item met count', () => {
63 const likes = msgs.filter((m) => m.type === 'like');
64 assert.equal(likes.length, 1, 'likes horen gegroepeerd te zijn');
65 assert.equal(likes[0].count, 3);
66 assert.equal(likes[0].actors.length, 3);
67});
68
69test('privé-reply draagt visibility voor de badge en heeft post-context', () => {
70 const thread = msgs.find((m) => m.type === 'thread');
71 const reply = thread.messages.find((m) => m.type === 'reply');
72 assert.equal(reply.visibility, 'direct');
73 assert.equal(reply.post_slug, 'mijn-post');
74 assert.equal(reply.post_title, 'Mijn post');
75 // De context hangt ook aan de draad zelf: die voedt de link in de kop.
76 assert.deepEqual(thread.post, { slug: 'mijn-post', title: 'Mijn post' });
77});
78
79test('notificationsSeenAt: 0 zonder watermark, daarna > 0', () => {
80 assert.equal(AP.notificationsSeenAt('me'), 0);
81 AP.markNotificationsSeen('me');
82 assert.ok(AP.notificationsSeenAt('me') > 0);
83});
84
85// ── Poll afgelopen → "resultaten zijn binnen"-item met tally ─────────────
86test('een afgelopen eigen peiling levert een poll_done-item met tally', () => {
87 db.prepare(`INSERT INTO posts (id, site_id, slug, author_id, title, content, status, type, poll_json, created_at, updated_at, published_at)
88 VALUES ('pp','s1','fav','u1','Favoriet?','<p>kies</p>','published','post',?,datetime('now'),datetime('now'),datetime('now'))`)
89 .run(JSON.stringify({ multiple: false, options: [{ name: 'A' }, { name: 'B' }], endTime: '2026-07-19T00:00:00Z', closed: true }));
90 const iv = db.prepare('INSERT OR IGNORE INTO poll_votes (post_id, actor_uri, choice) VALUES (?,?,?)');
91 iv.run('pp', 'https://r.test/u/x', 'A');
92 iv.run('pp', 'https://r.test/u/y', 'A');
93 iv.run('pp', 'https://r.test/u/z', 'B');
94
95 const fresh = AP.getMessages('me', 50);
96 const done = fresh.find((m) => m.type === 'poll_done');
97 assert.ok(done, 'poll_done-item ontbreekt');
98 assert.equal(done.post_slug, 'fav');
99 assert.equal(done.poll.voters, 3);
100 const a = done.poll.options.find((o) => o.name === 'A');
101 assert.equal(a.count, 2);
102 assert.equal(a.pct, 67); // round(2/3*100)
103});
104
105// Een nog lopende peiling (closed != 1) verschijnt NIET.
106test('een lopende peiling levert géén poll_done', () => {
107 db.prepare(`INSERT INTO posts (id, site_id, slug, author_id, title, content, status, type, poll_json, created_at, updated_at, published_at)
108 VALUES ('pp2','s1','open','u1','Open?','<p>kies</p>','published','post',?,datetime('now'),datetime('now'),datetime('now'))`)
109 .run(JSON.stringify({ multiple: false, options: [{ name: 'A' }, { name: 'B' }], endTime: '2030-01-01T00:00:00Z', closed: false }));
110 const fresh = AP.getMessages('me', 50);
111 assert.ok(!fresh.some((m) => m.type === 'poll_done' && m.post_slug === 'open'), 'lopende peiling hoort niet als poll_done te tonen');
112});
Note: See TracBrowser for help on using the repository browser.