Changeset 2a10445 in Klonkt
- Timestamp:
- 07/30/2026 08:23:47 AM (6 weeks ago)
- Branches:
- main
- Children:
- 094f7d0
- Parents:
- 15f1cb5
- Files:
-
- 5 edited
-
src/config/database.js (modified) (1 diff)
-
src/services/ActivityPubService.js (modified) (3 diffs)
-
src/views/partials/post-card.ejs (modified) (2 diffs)
-
src/views/partials/post-tile.ejs (modified) (2 diffs)
-
test/c2s-compose.test.js (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/config/database.js
r15f1cb5 r2a10445 696 696 ensureColumn('ap_gated_offers', 'proposer', 'TEXT'); // who proposed (5.6): the settle-answer goes back to them 697 697 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 */ } 698 705 ensureColumn('ap_mentions', 'wave', 'INTEGER'); // inbound guardian wave 699 706 // FEP-633c §2.2: object hint that the author is a ward. Register-only for now; -
src/services/ActivityPubService.js
r15f1cb5 r2a10445 2410 2410 return `<p><video controls playsinline preload="metadata"${poster} src="${a.url}"></video></p>`; 2411 2411 }).join(''); 2412 // The web tile (Robins aanwijzing, 30-7): a video post's first video2413 // becomes the cover video, so the grid plays it muted-looping as its own2414 // thumbnail, exactly like an animated cover. Deliberately NO cover image2415 // next to it: the tile's image branch would win and freeze the tile. A2416 // 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/'));2420 2412 const postId = crypto.randomUUID(); 2421 2413 const slug = 'n-' + postId.slice(0, 8); … … 2428 2420 const vis = c2sVisibility(object); 2429 2421 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); 2434 2428 if (media.length) { try { db.prepare('UPDATE posts SET c2s_attachments = ? WHERE id = ?').run(JSON.stringify(media), postId); } catch { /* column exists via ensureColumn */ } } 2435 2429 try { db.prepare('UPDATE posts SET content_rendered = ? WHERE id = ?').run(bakePostContent(html + mediaHtml), postId); } catch { /* render fallback covers it */ } … … 2437 2431 try { db.prepare('INSERT INTO posts_fts(content, title, author, post_id) VALUES (?,?,?,?)').run(HtmlSanitizerService.toPlainText(html), '', user.username || '', postId); } catch { /* FTS non-fatal */ } 2438 2432 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, c over_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 */ }); 2440 2434 } 2441 2435 return { status: 201, id: postId, url: `${base}/ap/notes/${postId}` }; -
src/views/partials/post-card.ejs
r15f1cb5 r2a10445 16 16 const _ext = !!_external; 17 17 const _src = post.source_name || ''; 18 const _hasCover = !!post.cover_image_url || !!post.cover_video_url; 18 const _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. 21 let _cVideo = null, _cPoster = null, _cImg = null; 22 if (!_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 } 27 const _hasCover = _realCover || !!_cVideo || !!_cImg; 19 28 const _typeLabel = (post.type && post.type !== 'overig' && post.type !== 'post') ? post.type : ''; 20 29 const _isPinned = !!post.pinned; … … 53 62 sizes="(min-width: 768px) 120px, 100vw" 54 63 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"><% } %> 56 65 <% if (post.nsfw) { %><span class="nsfw-veil"><%- include('nsfw-veil', { cw: post.content_warning }) %></span><% } %> 57 66 </a> -
src/views/partials/post-tile.ejs
r15f1cb5 r2a10445 7 7 const _isBoost = !!post.isBoost; 8 8 const _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>. 13 let _cVideo = null, _cPoster = null, _cImg = null; 14 if (!_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 } 19 const _showCover = _hasCover || !!_cVideo || !!_cImg; 9 20 const _typeLabel = (post.type && post.type !== 'post') ? post.type : ''; 10 21 const _src = post.source_name || ''; // bron-site (cirkel-feed: van welke site komt deze post) … … 14 25 const _ext = !!_external; 15 26 %> 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' : '' %>" 17 28 href="<%= _href %>" 18 29 style="--tile-hue: <%= _tileHue %>;" 19 30 <% 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"<% } %>> 20 31 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"><% } %> 22 33 23 34 <% if (_typeLabel) { %> -
test/c2s-compose.test.js
r15f1cb5 r2a10445 85 85 assert.equal(vid.icon && vid.icon.url, 'https://test.example/media/reply-media/film.mp4.poster.jpg', 86 86 '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); 91 91 assert.equal(post.cover_image_url, null); 92 92 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'); 94 94 } finally { 95 95 if (prev === undefined) delete process.env.MEDIA_PATH; else process.env.MEDIA_PATH = prev; … … 123 123 assert.equal(r.status, 201, 'a picture can be the whole message'); 124 124 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'); 127 126 });
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)