Changeset 2a10445 in Klonkt


Ignore:
Timestamp:
07/30/2026 08:23:47 AM (6 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
094f7d0
Parents:
15f1cb5
Message:

De tegel leest zijn beeld uit de content, de cover-kopie is weg

Robins melding: de video stond dubbel op de postpagina, een keer afspeelbaar
in de content en een keer als cover erboven. Terecht; de cover-promotie van
vanmiddag was de verkeerde helft van de oplossing.

Nu doet de tegel wat de Cirkel-kaart al deed: zijn beeld AFLEIDEN uit de
content. post-tile en post-card pakken de eerste video (met zijn poster) of
de eerste afbeelding uit post.content als er geen echte cover is, en de
grid-tegel speelt de video muted-loopend als zijn eigen thumbnail. Het
post-model blijft single-source: c2sCreatePost zet geen covers meer.

En zelfherstel voor wat er al staat: covers die hun eigen content-media
dupliceren (het korte cover-promotie-venster) worden bij boot leeggemaakt;
idempotent en raakt alleen die gevallen.

Changed files:
src/services/ActivityPubService.js

  • c2sCreatePost zet geen covers meer; media leeft alleen in de content

src/views/partials/post-tile.ejs

  • content-afgeleide fallback: video (met poster) of afbeelding

src/views/partials/post-card.ejs

  • dezelfde fallback in de lijstweergave

src/config/database.js

  • zelfherstel: cover-kopieen van content-media leeggemaakt

test/c2s-compose.test.js

  • covers blijven null; de video federeert precies een keer

remarks: 338 tests groen, server start.

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

Files:
5 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    r15f1cb5 r2a10445  
    696696  ensureColumn('ap_gated_offers', 'proposer', 'TEXT'); // who proposed (5.6): the settle-answer goes back to them
    697697  ensureColumn('posts', 'c2s_attachments', 'TEXT'); // media a C2S Note carried (JSON [{url,mediaType,name}]); buildNote federates them
     698  // 30-7: C2S posts briefly got their content media copied onto the cover,
     699  // which showed the same video twice on the post page. Clear the covers that
     700  // duplicate their own content; idempotent, only ever touches those.
     701  try {
     702    db.prepare("UPDATE posts SET cover_video_url = NULL WHERE cover_video_url LIKE '/media/reply-media/%' AND instr(content, cover_video_url) > 0").run();
     703    db.prepare("UPDATE posts SET cover_image_url = NULL WHERE cover_image_url LIKE '/media/reply-media/%' AND instr(content, cover_image_url) > 0").run();
     704  } catch { /* posts table absent on fresh init */ }
    698705  ensureColumn('ap_mentions', 'wave', 'INTEGER');  // inbound guardian wave
    699706  // FEP-633c §2.2: object hint that the author is a ward. Register-only for now;
  • src/services/ActivityPubService.js

    r15f1cb5 r2a10445  
    24102410    return `<p><video controls playsinline preload="metadata"${poster} src="${a.url}"></video></p>`;
    24112411  }).join('');
    2412   // The web tile (Robins aanwijzing, 30-7): a video post's first video
    2413   // becomes the cover video, so the grid plays it muted-looping as its own
    2414   // thumbnail, exactly like an animated cover. Deliberately NO cover image
    2415   // next to it: the tile's image branch would win and freeze the tile. A
    2416   // photo post gets its first image as cover, instead of the (untitled)
    2417   // gradient.
    2418   const coverVid = media.find((a) => a.mediaType.startsWith('video/'));
    2419   const coverImg = media.find((a) => a.mediaType.startsWith('image/'));
    24202412  const postId = crypto.randomUUID();
    24212413  const slug = 'n-' + postId.slice(0, 8);
     
    24282420  const vis = c2sVisibility(object);
    24292421  const fanOnly = (vis === 'friends' || vis === 'direct') ? 1 : 0;
    2430   db.prepare(`INSERT INTO posts (id, site_id, slug, author_id, title, content, excerpt, status, type, language, fan_only, ap_visibility, cover_image_url, cover_video_url, created_at, updated_at, published_at)
    2431               VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
    2432     .run(postId, site.id, slug, user.id, '', html + mediaHtml, '', 'published', 'post', object.language || 'nl', fanOnly, vis,
    2433       coverVid ? null : (coverImg ? coverImg.url : null), coverVid ? coverVid.url : null, now, now, now);
     2422  // Deliberately NO cover (Robins besluit, 30-7): the media lives in the
     2423  // content, and a cover next to it showed the same video twice on the post
     2424  // page. The tiles derive their picture from the content instead.
     2425  db.prepare(`INSERT INTO posts (id, site_id, slug, author_id, title, content, excerpt, status, type, language, fan_only, ap_visibility, created_at, updated_at, published_at)
     2426              VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
     2427    .run(postId, site.id, slug, user.id, '', html + mediaHtml, '', 'published', 'post', object.language || 'nl', fanOnly, vis, now, now, now);
    24342428  if (media.length) { try { db.prepare('UPDATE posts SET c2s_attachments = ? WHERE id = ?').run(JSON.stringify(media), postId); } catch { /* column exists via ensureColumn */ } }
    24352429  try { db.prepare('UPDATE posts SET content_rendered = ? WHERE id = ?').run(bakePostContent(html + mediaHtml), postId); } catch { /* render fallback covers it */ }
     
    24372431  try { db.prepare('INSERT INTO posts_fts(content, title, author, post_id) VALUES (?,?,?,?)').run(HtmlSanitizerService.toPlainText(html), '', user.username || '', postId); } catch { /* FTS non-fatal */ }
    24382432  if (vis !== 'direct') {
    2439     deliverCreate(site, { id: postId, slug, title: '', content: html + mediaHtml, published_at: now, created_at: now, fan_only: fanOnly, ap_visibility: vis, cover_image_url: coverVid ? null : (coverImg ? coverImg.url : null), cover_video_url: coverVid ? coverVid.url : null, c2s_attachments: media.length ? JSON.stringify(media) : null }).catch(() => { /* best-effort */ });
     2433    deliverCreate(site, { id: postId, slug, title: '', content: html + mediaHtml, published_at: now, created_at: now, fan_only: fanOnly, ap_visibility: vis, c2s_attachments: media.length ? JSON.stringify(media) : null }).catch(() => { /* best-effort */ });
    24402434  }
    24412435  return { status: 201, id: postId, url: `${base}/ap/notes/${postId}` };
  • src/views/partials/post-card.ejs

    r15f1cb5 r2a10445  
    1616const _ext       = !!_external;
    1717const _src       = post.source_name || '';
    18 const _hasCover  = !!post.cover_image_url || !!post.cover_video_url;
     18const _realCover = !!post.cover_image_url || !!post.cover_video_url;
     19// Content-derived fallback (see post-tile.ejs): a C2S post's media lives in
     20// its content, and the card fronts it from there.
     21let _cVideo = null, _cPoster = null, _cImg = null;
     22if (!_realCover && post.content) {
     23  const vm = String(post.content).match(/<video[^>]*\ssrc="([^"]+)"[^>]*>/i);
     24  if (vm) { _cVideo = vm[1]; const pm = vm[0].match(/poster="([^"]+)"/i); if (pm) _cPoster = pm[1]; }
     25  else { const im = String(post.content).match(/<img[^>]*\ssrc="([^"]+)"/i); if (im) _cImg = im[1]; }
     26}
     27const _hasCover  = _realCover || !!_cVideo || !!_cImg;
    1928const _typeLabel = (post.type && post.type !== 'overig' && post.type !== 'post') ? post.type : '';
    2029const _isPinned  = !!post.pinned;
     
    5362           sizes="(min-width: 768px) 120px, 100vw"
    5463           alt="" loading="lazy" decoding="async"<% if (post.cover_video_url) { %> data-ios-mp4="<%= post.cover_video_url %>"<% } %>>
    55       <% } else if (post.cover_video_url) { %><video src="<%= post.cover_video_url %>" poster="<%= thumb(post.cover_video_url, 480) %>" autoplay loop muted playsinline></video><% } %>
     64      <% } else if (post.cover_video_url) { %><video src="<%= post.cover_video_url %>" poster="<%= thumb(post.cover_video_url, 480) %>" autoplay loop muted playsinline></video><% } else if (_cVideo) { %><video src="<%= _cVideo %>"<% if (_cPoster) { %> poster="<%= _cPoster %>"<% } %> autoplay loop muted playsinline preload="metadata"></video><% } else if (_cImg) { %><img src="<%= _cImg %>" alt="" loading="lazy" decoding="async"><% } %>
    5665      <% if (post.nsfw) { %><span class="nsfw-veil"><%- include('nsfw-veil', { cw: post.content_warning }) %></span><% } %>
    5766    </a>
  • src/views/partials/post-tile.ejs

    r15f1cb5 r2a10445  
    77const _isBoost = !!post.isBoost;
    88const _hasCover = !!post.cover_image_url || !!post.cover_video_url;
     9// A C2S post carries its media IN the content (a cover next to it showed the
     10// video twice on the post page). The tile derives its picture from the
     11// content instead, like the Cirkel card: first <video> (with its poster) or
     12// first <img>.
     13let _cVideo = null, _cPoster = null, _cImg = null;
     14if (!_hasCover && post.content) {
     15  const vm = String(post.content).match(/<video[^>]*\ssrc="([^"]+)"[^>]*>/i);
     16  if (vm) { _cVideo = vm[1]; const pm = vm[0].match(/poster="([^"]+)"/i); if (pm) _cPoster = pm[1]; }
     17  else { const im = String(post.content).match(/<img[^>]*\ssrc="([^"]+)"/i); if (im) _cImg = im[1]; }
     18}
     19const _showCover = _hasCover || !!_cVideo || !!_cImg;
    920const _typeLabel = (post.type && post.type !== 'post') ? post.type : '';
    1021const _src = post.source_name || '';   // bron-site (cirkel-feed: van welke site komt deze post)
     
    1425const _ext = !!_external;
    1526%>
    16 <a class="grid-tile<%= _hasCover ? '' : ' grid-tile-gradient' %><%= (_isPinned || _isBoost) ? ' is-pinned' : '' %><%= post.nsfw ? ' nsfw-media' : '' %>"
     27<a class="grid-tile<%= _showCover ? '' : ' grid-tile-gradient' %><%= (_isPinned || _isBoost) ? ' is-pinned' : '' %><%= post.nsfw ? ' nsfw-media' : '' %>"
    1728   href="<%= _href %>"
    1829   style="--tile-hue: <%= _tileHue %>;"
    1930   <% if (_ext) { %>target="_blank" rel="noopener"<% } else { %>hx-get="<%= _href %>?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" hx-push-url="<%= _href %>" hx-indicator="#pcms-loading"<% } %>>
    2031
    21   <% if (post.cover_image_url) { %><img class="grid-tile-img" src="<%= thumb(post.cover_image_url, 480) %>" alt="" loading="lazy" decoding="async"<% if (post.cover_video_url) { %> data-ios-mp4="<%= post.cover_video_url %>"<% } %>><% } else if (post.cover_video_url) { %><video class="grid-tile-img" src="<%= post.cover_video_url %>" poster="<%= thumb(post.cover_video_url, 480) %>" autoplay loop muted playsinline></video><% } %>
     32  <% if (post.cover_image_url) { %><img class="grid-tile-img" src="<%= thumb(post.cover_image_url, 480) %>" alt="" loading="lazy" decoding="async"<% if (post.cover_video_url) { %> data-ios-mp4="<%= post.cover_video_url %>"<% } %>><% } else if (post.cover_video_url) { %><video class="grid-tile-img" src="<%= post.cover_video_url %>" poster="<%= thumb(post.cover_video_url, 480) %>" autoplay loop muted playsinline></video><% } else if (_cVideo) { %><video class="grid-tile-img" src="<%= _cVideo %>"<% if (_cPoster) { %> poster="<%= _cPoster %>"<% } %> autoplay loop muted playsinline preload="metadata"></video><% } else if (_cImg) { %><img class="grid-tile-img" src="<%= _cImg %>" alt="" loading="lazy" decoding="async"><% } %>
    2233
    2334  <% if (_typeLabel) { %>
  • test/c2s-compose.test.js

    r15f1cb5 r2a10445  
    8585    assert.equal(vid.icon && vid.icon.url, 'https://test.example/media/reply-media/film.mp4.poster.jpg',
    8686      'the poster federates as the attachment icon');
    87     // The web tile plays the video as its own thumbnail (Robins aanwijzing):
    88     // the first video becomes the cover video, with NO cover image beside it,
    89     // because the tile's image branch would win and freeze the tile.
    90     assert.equal(post.cover_video_url, '/media/reply-media/film.mp4');
     87    // NO cover (Robins besluit): a cover next to the content showed the same
     88    // video twice on the post page. The tiles derive their picture from the
     89    // content (post-tile/post-card), so the post model stays single-source.
     90    assert.equal(post.cover_video_url, null);
    9191    assert.equal(post.cover_image_url, null);
    9292    assert.equal((note.attachment || []).filter((a) => a.url.endsWith('film.mp4')).length, 1,
    93       'cover video + attachment dedupe to the one entry that knows its poster');
     93      'and the video federates exactly once');
    9494  } finally {
    9595    if (prev === undefined) delete process.env.MEDIA_PATH; else process.env.MEDIA_PATH = prev;
     
    123123  assert.equal(r.status, 201, 'a picture can be the whole message');
    124124  const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(r.id);
    125   assert.equal(post.cover_image_url, '/media/reply-media/alleen.jpg',
    126     'a photo post fronts its photo instead of the (untitled) gradient');
     125  assert.equal(post.cover_image_url, null, 'no cover: the tile reads the photo from the content');
    127126});
Note: See TracChangeset for help on using the changeset viewer.