source: Klonkt/test/note-body-shared.test.js

main
Last change on this file was d9ad6c5, checked in by Robin Genis <roboburr@…>, 6 weeks ago

Een hulpvraag hoort in Berichten, niet in de Krant

Een 🛟 van een ward kwam bij de guardian op twee plekken binnen: als mention in
Berichten en de Guardian PWA, maar ook als gewone post in de Krant. Op
sound-fabrics.com stonden vijf van de zes hulpvragen in allebei.

De oorzaak zat in de inbox: de tijdlijn-insert vroeg alleen "is dit een
top-level post van iemand die ik volg" en keek niet naar wie de post geadresseerd
was. Dat is nu belongsInTimeline: een directe note is aan iemand persoonlijk
gericht, dus een bericht en geen post. Dat dekt meteen de wave en de gewone DM,
die om dezelfde reden in de Krant terechtkwamen. De self-heal ruimt de al
opgeslagen exemplaren op, beperkt tot de twee soorten die achteraf nog te
herkennen zijn; een publieke mention van iemand die je volgt is wel een post en
blijft staan.

Tweede helft: de weergave gelijkgetrokken. De Krant rendert een post met
emoji's, een quote- of linkkaart en de media; Berichten liet daar niks van zien
(zelfs de shortcodes bleven staan, want ap_mentions had geen emoji_json) en de
Guardian PWA plakte de kale content in een div. Die opmaak zat bovendien in de
<style> van de Krant zelf, dus een post buiten de Krant kwam sowieso ongestyled
binnen.

Nu is er één partial, note-body, met de opmaak in shared-styles ernaast. Alle
drie de oppervlakken gebruiken hem: de PWA bouwt zijn kaarten in de browser en
krijgt de body server-side gerenderd mee. ap_mentions en ap_interactions kregen
de kolommen die daarvoor nodig zijn, gevuld bij binnenkomst, met de quote- of
linkkaart out of band zoals de tijdlijn dat al deed.

En passant: de embed-gate stond alleen op de C2S-read, dus een ward zag in de
web-Krant nog steeds linkvoorbeelden die de guardians hadden uitgezet. Die gate
zit nu ook op /news en /messages.

Changed files:
src/services/ActivityPubService.js

  • belongsInTimeline: een directe note is geen tijdlijn-post
  • self-heal v21 verwijdert al opgeslagen hulpvragen en waves uit ap_timeline
  • resolveCard: quote of linkvoorbeeld, één kaart, out of band opgelost
  • mentions en replies slaan emoji's, media en die kaart op
  • getNotifications geeft die kolommen door aan Berichten

src/config/database.js

  • kolommen op ap_mentions en ap_interactions voor emoji's, media, quote, embed

src/middleware/render.js

  • renderNoteBody: dezelfde partial als string, voor niet-EJS oppervlakken

src/routes/posts.js

  • gateEmbeds op /news en /messages (FEP-633c gated feature)

src/routes/guardian.js

  • hulpvragen krijgen body_html en name_html mee

src/assets/js/guardian.js

  • kaart rendert die body in plaats van de kale content

src/assets/css/guardian.css

  • opmaak voor de gedeelde post-body in de kleuren van de PWA

src/views/partials/tl-item.ejs

  • body vervangen door de gedeelde partial

src/views/partials/msg-item.ejs

  • idem, plus een 🛟-markering bij een hulpvraag

src/views/partials/shared-styles.ejs

  • .tl-content, .tl-quote* en .tl-media* hierheen verhuisd

src/views/pages/news.ejs

  • die regels weggehaald, alleen Krant-eigen opmaak blijft

src/services/i18n.js

  • msg.help_request in nl, en, de

New file:
src/views/partials/note-body.ejs

  • de body van een post: content, quote/linkkaart, media

test/help-request-timeline.test.js

  • een 🛟 blijft uit de Krant en in Berichten

test/note-body-shared.test.js

  • één renderer, en beide views gaan er doorheen

remarks: geverifieerd op een wegwerp-database in de browser: de hulpvraag
verdwijnt bij het opstarten uit de Krant en staat mét quote en capture in
Berichten en de PWA. Nog niet uitgerold.

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

  • Property mode set to 100644
File size: 4.6 KB
RevLine 
[d9ad6c5]1// One post, one rendering. De Krant, Berichten and the Guardian PWA all run a
2// note through partials/note-body, so a quote card, a link preview, the media
3// and the custom emojis show up wherever the post turns up — not only in the
4// feed it happened to arrive in.
5import { test } from 'node:test';
6import assert from 'node:assert/strict';
7import fs from 'fs';
8import path from 'path';
9
10process.env.DATABASE_PATH = ':memory:';
11process.env.PUBLIC_BASE_URL = 'https://test.example';
12
13const dbMod = await import('../src/config/database.js');
14const db = dbMod.default;
15dbMod.initializeDatabase();
16const { renderNoteBody } = await import('../src/middleware/render.js');
17const AP = (await import('../src/services/ActivityPubService.js')).default;
18
19const VIEWS = path.join(process.cwd(), 'src', 'views', 'partials');
20const EMOJI = JSON.stringify({ ':party:': 'https://cdn.test/party.png' });
21const MEDIA = JSON.stringify([{ url: 'https://cdn.test/capture.png', type: 'image/png' }]);
22const QUOTE = JSON.stringify({ url: 'https://q.test/notes/7', author: { name: 'Opie', handle: '@opie@q.test' }, content: '<p>het origineel</p>', media: [] });
23const EMBED = JSON.stringify({ url: 'https://video.test/watch?v=1', title: 'Een filmpje', provider: 'video.test', media: [{ url: 'https://video.test/thumb.jpg', type: 'image/jpeg' }] });
24
25test('the content renders with its custom emojis', () => {
26 const html = renderNoteBody({ content: '<p>hoi :party:</p>', emoji_json: EMOJI }, 'nl');
27 assert.match(html, /class="tl-content"/);
28 assert.match(html, /<img[^>]+class="emoji"[^>]+party\.png/, ':party: became an image, not a shortcode');
29});
30
31test('a quoted post renders as the quote card', () => {
32 const html = renderNoteBody({ content: '<p>kijk</p>', quote_json: QUOTE }, 'nl');
33 assert.match(html, /class="tl-quote"/);
34 assert.match(html, /het origineel/);
35 assert.match(html, /@opie@q\.test/);
36});
37
38test('an external link preview renders as that same card, with the title escaped', () => {
39 const html = renderNoteBody({ content: '<p>kijk</p>', embed_json: EMBED }, 'nl');
40 assert.match(html, /class="tl-quote"/, 'one card for both, only the origin differs');
41 assert.match(html, /Een filmpje/);
42 assert.ok(!/<iframe/i.test(html), 'a preview is a thumbnail, never an embedded player');
43});
44
45test('a quote wins over a link preview: only one card', () => {
46 const html = renderNoteBody({ content: '<p>x</p>', quote_json: QUOTE, embed_json: EMBED }, 'nl');
47 assert.equal((html.match(/class="tl-quote"/g) || []).length, 1);
48 assert.match(html, /het origineel/);
49 assert.ok(!html.includes('Een filmpje'));
50});
51
52test('media renders, and a sensitive note keeps its veil', () => {
53 const plain = renderNoteBody({ content: '<p>x</p>', media_json: MEDIA }, 'nl');
54 assert.match(plain, /class="tl-media-img"/);
55 const nsfw = renderNoteBody({ content: '<p>x</p>', media_json: MEDIA, nsfw: 1, cw: 'spoiler' }, 'nl');
56 assert.match(nsfw, /nsfw-media/, 'the veil survives outside de Krant too');
57});
58
59test('an empty note renders nothing at all', () => {
60 assert.equal(renderNoteBody({ content: '' }, 'nl'), '');
61 assert.equal(renderNoteBody(null, 'nl'), '');
62});
63
64test('de Krant and Berichten both go through the shared partial', () => {
65 for (const f of ['tl-item.ejs', 'msg-item.ejs']) {
66 const src = fs.readFileSync(path.join(VIEWS, f), 'utf8');
67 assert.match(src, /partials\/note-body/, `${f} must render a post through the shared partial`);
68 assert.ok(!/class="tl-media-img"/.test(src), `${f} must not carry its own copy of the media markup`);
69 }
70});
71
72test('Berichten receives the columns that partial needs', () => {
73 db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@test', 'x', 'god');
74 db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,?)').run('s1', 'me', 'Me', 'u1', 1);
75 db.prepare(`INSERT INTO ap_mentions (slug, object_uri, actor_uri, actor_name, actor_handle, content, emoji_json, media_json, quote_json, help_request)
76 VALUES ('me','https://r.test/notes/1','https://r.test/u/a','Anna','@a@r.test','<p>hoi :party:</p>',?,?,?,1)`).run(EMOJI, MEDIA, QUOTE);
77 const m = AP.getNotifications('me', 20).find((n) => n.type === 'mention');
78 assert.ok(m);
79 assert.equal(m.emoji_json, EMOJI);
80 assert.equal(m.media_json, MEDIA);
81 assert.equal(m.quote_json, QUOTE);
82 assert.equal(m.help_request, 1, 'so Berichten can mark a 🛟 as one');
83 // The proof: the same object, handed to the same renderer, comes out whole.
84 const html = renderNoteBody(m, 'nl');
85 assert.match(html, /party\.png/);
86 assert.match(html, /class="tl-quote"/);
87 assert.match(html, /class="tl-media-img"/);
88});
Note: See TracBrowser for help on using the repository browser.