source: Klonkt/test/messages-conversations.test.js@ 7875a80

main
Last change on this file since 7875a80 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: 5.6 KB
Line 
1// Berichten als gesprekken: antwoorden, mentions en je eigen verzonden
2// berichten vouwen samen tot draden, de rest van de stroom blijft ongemoeid.
3// Dit dekt de groepeerlogica zelf (zuiver, geen DB, geen netwerk); het
4// samenstellen van de stroom zit in getMessages en leunt op de database.
5//
6// Run: npm test
7
8import { test } from 'node:test';
9import assert from 'node:assert/strict';
10
11process.env.DATABASE_PATH = ':memory:';
12process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
13
14const dbMod = await import('../src/config/database.js');
15dbMod.initializeDatabase();
16const AP = await import('../src/services/ActivityPubService.js');
17
18const { threadKey, groupConversations } = AP;
19
20// Aflopend gesorteerd, zoals getMessages ze aanlevert.
21const ts = (n) => new Date(Date.UTC(2026, 0, 1, 12, n)).toISOString();
22
23test('threadKey: een post wint van een persoon', () => {
24 assert.equal(threadKey({ type: 'reply', post_slug: 'hallo', handle: '@anna@a.test' }), 'post:hallo');
25 assert.equal(threadKey({ type: 'sent', post_slug: 'hallo', to_handle: '@anna@a.test' }), 'post:hallo');
26 // Zonder post loopt de draad per tegenpartij, hoofdletter- en @-ongevoelig.
27 assert.equal(threadKey({ type: 'mention', handle: '@Anna@A.test' }), 'actor:anna@a.test');
28 assert.equal(threadKey({ type: 'sent', to_handle: 'Anna@a.test' }), 'actor:anna@a.test');
29});
30
31test('threadKey: alles wat geen gesprek is krijgt geen draad', () => {
32 for (const type of ['like', 'announce', 'follow', 'report', 'poll_done']) {
33 assert.equal(threadKey({ type, post_slug: 'hallo' }), null, `${type} hoort geen draad te krijgen`);
34 }
35 assert.equal(threadKey(null), null);
36 assert.equal(threadKey({ type: 'mention' }), null, 'een mention zonder afzender heeft geen sleutel');
37});
38
39test('ontvangen en verzonden op dezelfde post komen in EEN draad', () => {
40 const out = groupConversations([
41 { type: 'sent', post_slug: 'hallo', content: 'graag gedaan', created_at: ts(3) },
42 { type: 'reply', post_slug: 'hallo', post_title: 'Hallo fediverse', handle: '@anna@a.test', name: 'Anna', content: 'dank!', created_at: ts(2) },
43 { type: 'reply', post_slug: 'hallo', post_title: 'Hallo fediverse', handle: '@bo@b.test', name: 'Bo', content: 'mooi', created_at: ts(1) },
44 ]);
45 assert.equal(out.length, 1, 'drie berichten, één draad');
46 const t = out[0];
47 assert.equal(t.type, 'thread');
48 assert.equal(t.count, 3);
49 // De context hoort bij de draad: dit gesprek gaat over een post.
50 assert.deepEqual(t.post, { slug: 'hallo', title: 'Hallo fediverse' });
51 // Een gesprek leest naar beneden: oud → nieuw.
52 assert.deepEqual(t.messages.map((m) => m.content), ['mooi', 'dank!', 'graag gedaan']);
53 // De draad staat op de tijd van zijn NIEUWSTE bericht, zodat een levend
54 // gesprek bovenaan komt.
55 assert.equal(t.created_at, ts(3));
56 // Jij zit er niet bij als deelnemer, de anderen wel.
57 assert.deepEqual(t.people.map((p) => p.name), ['Bo', 'Anna']);
58 assert.equal(t.mine, true);
59});
60
61test('een mention zonder post loopt per persoon, niet per post', () => {
62 const out = groupConversations([
63 { type: 'sent', to_handle: '@cas@c.test', content: 'hoi terug', created_at: ts(2) },
64 { type: 'mention', handle: '@cas@c.test', name: 'Cas', content: 'hoi!', created_at: ts(1) },
65 ]);
66 assert.equal(out.length, 1);
67 assert.equal(out[0].key, 'actor:cas@c.test');
68 assert.equal(out[0].post, null, 'zonder post is er geen context-link');
69 assert.deepEqual(out[0].messages.map((m) => m.content), ['hoi!', 'hoi terug']);
70});
71
72test('twee posts blijven twee draden, op volgorde van hun nieuwste bericht', () => {
73 const out = groupConversations([
74 { type: 'reply', post_slug: 'twee', post_title: 'Twee', handle: '@anna@a.test', created_at: ts(4) },
75 { type: 'reply', post_slug: 'een', post_title: 'Een', handle: '@bo@b.test', created_at: ts(3) },
76 { type: 'reply', post_slug: 'twee', post_title: 'Twee', handle: '@bo@b.test', created_at: ts(2) },
77 ]);
78 assert.deepEqual(out.map((t) => t.key), ['post:twee', 'post:een']);
79 assert.equal(out[0].count, 2);
80 assert.equal(out[1].count, 1);
81});
82
83test('likes, boosts en follows stromen ongemoeid tussen de draden door', () => {
84 const out = groupConversations([
85 { type: 'like', post_slug: 'hallo', handle: '@anna@a.test', created_at: ts(4) },
86 { type: 'reply', post_slug: 'hallo', post_title: 'Hallo', handle: '@bo@b.test', created_at: ts(3) },
87 { type: 'follow', handle: '@cas@c.test', created_at: ts(2) },
88 { type: 'report', handle: '@dee@d.test', created_at: ts(1) },
89 ]);
90 assert.deepEqual(out.map((i) => i.type), ['like', 'thread', 'follow', 'report']);
91 // Een like op dezelfde post trekt die post NIET de draad in: het is geen
92 // gesprek, en Activiteit blijft zijn eigen chip houden.
93 assert.equal(out[0].post_slug, 'hallo');
94});
95
96test('de titel komt van welk bericht in de draad hem ook maar kent', () => {
97 // Een verzonden antwoord kent alleen de slug; een ontvangen antwoord de titel.
98 const out = groupConversations([
99 { type: 'sent', post_slug: 'hallo', created_at: ts(2) },
100 { type: 'reply', post_slug: 'hallo', post_title: 'Hallo fediverse', handle: '@anna@a.test', created_at: ts(1) },
101 ]);
102 assert.deepEqual(out[0].post, { slug: 'hallo', title: 'Hallo fediverse' });
103});
104
105test('lege en rommelige invoer levert geen kapotte draden op', () => {
106 assert.deepEqual(groupConversations([]), []);
107 assert.deepEqual(groupConversations(null), []);
108 // Een gesprekssoort zonder sleutel blijft een losse regel in plaats van te
109 // verdwijnen: nooit een bericht kwijtraken in de groepering.
110 const out = groupConversations([{ type: 'sent', content: 'aan niemand', created_at: ts(1) }]);
111 assert.equal(out.length, 1);
112 assert.equal(out[0].type, 'sent');
113});
Note: See TracBrowser for help on using the repository browser.