Changeset b7d4458 in Klonkt
- Timestamp:
- 06/27/2026 08:10:43 AM (2 months ago)
- Branches:
- main
- Children:
- c21197a
- Parents:
- 92f6976
- Location:
- src
- Files:
-
- 11 edited
-
config/database.js (modified) (2 diffs)
-
routes/activitypub.js (modified) (2 diffs)
-
routes/circle.js (modified) (1 diff)
-
routes/posts.js (modified) (8 diffs)
-
services/ActivityPubService.js (modified) (7 diffs)
-
services/Scheduler.js (modified) (2 diffs)
-
services/i18n.js (modified) (3 diffs)
-
views/pages/post-edit.ejs (modified) (1 diff)
-
views/pages/post.ejs (modified) (1 diff)
-
views/partials/post-card.ejs (modified) (1 diff)
-
views/partials/post-tile.ejs (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/config/database.js
r92f6976 rb7d4458 85 85 ensureColumn('posts', 'fan_only', 'INTEGER DEFAULT 0'); // fan-only preview (premium #3) 86 86 ensureColumn('posts', 'nsfw', 'INTEGER DEFAULT 0'); // sensitive content → blur + click-to-reveal; fediverse sensitive 87 ensureColumn('posts', 'content_warning', 'TEXT'); // custom CW label (empty = default "Gevoelige inhoud") 87 88 ensureColumn('posts', 'type', "TEXT DEFAULT 'post'"); // post | foto | video | audio 88 89 … … 416 417 ensureColumn('ap_timeline', 'boosted', 'INTEGER DEFAULT 0'); 417 418 ensureColumn('ap_timeline', 'liked', 'INTEGER DEFAULT 0'); // a feed post you liked (⭐) → toggle 419 ensureColumn('ap_timeline', 'nsfw', 'INTEGER DEFAULT 0'); // remote sensitive post → blur in the Cirkel 420 ensureColumn('ap_timeline', 'cw', 'TEXT'); // remote content-warning text 418 421 } 419 422 -
src/routes/activitypub.js
r92f6976 rb7d4458 67 67 if (!site) return res.status(404).end(); 68 68 const posts = db.prepare( 69 `SELECT id, slug, title, content, cover_image_url, nsfw, published_at, created_at69 `SELECT id, slug, title, content, cover_image_url, nsfw, content_warning, published_at, created_at 70 70 FROM posts WHERE site_id = ? AND status = 'published' AND (fan_only IS NULL OR fan_only = 0) 71 71 ORDER BY COALESCE(published_at, created_at) DESC LIMIT 20` … … 90 90 // rank 1 last) → Mastodon flips it back to pin-rank ascending on the profile. 91 91 const posts = db.prepare( 92 `SELECT id, slug, title, content, cover_image_url, nsfw, published_at, created_at92 `SELECT id, slug, title, content, cover_image_url, nsfw, content_warning, published_at, created_at 93 93 FROM posts WHERE site_id = ? AND status = 'published' AND (fan_only IS NULL OR fan_only = 0) 94 94 AND pinned IS NOT NULL AND pinned > 0 -
src/routes/circle.js
r92f6976 rb7d4458 53 53 pinned: 0, 54 54 isBoost: !!r.boosted, // a post YOU boosted → render in the pinned style with a Boost badge 55 nsfw: r.nsfw ? 1 : 0, // remote sensitive post → blur in the Cirkel (post-card/tile) 56 content_warning: r.cw || '', 55 57 status: 'published', 56 58 source_name: name, -
src/routes/posts.js
r92f6976 rb7d4458 177 177 const fanOnly = req.body.fan_only ? 1 : 0; 178 178 const nsfw = req.body.nsfw ? 1 : 0; 179 const cw = (req.body.content_warning || '').trim().slice(0, 200); 179 180 180 181 // Content arrives as user-authored HTML from the WYSIWYG editor — sanitize … … 214 215 INSERT INTO posts ( 215 216 id, site_id, slug, author_id, title, content, excerpt, 216 status, cover_image_url, pinned, tags, type, noindex, fan_only, nsfw, publish_at,217 status, cover_image_url, pinned, tags, type, noindex, fan_only, nsfw, content_warning, publish_at, 217 218 created_at, updated_at, published_at 218 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )219 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 219 220 `).run( 220 221 postId, site.id, finalSlug, req.session.user.id, … … 222 223 finalStatus, cover_image_url || null, parsePinnedRank(pinned), 223 224 JSON.stringify((tags || '').split(',').map(t => t.trim()).filter(Boolean)), 224 finalType, noindex ? 1 : 0, fanOnly, nsfw, publishAt,225 finalType, noindex ? 1 : 0, fanOnly, nsfw, cw, publishAt, 225 226 now, now, publishedAt 226 227 ); … … 239 240 id: postId, slug: finalSlug, title: title || finalSlug, 240 241 content: cleanContent, cover_image_url: cover_image_url || null, 241 published_at: publishedAt, created_at: now, fan_only: fanOnly, nsfw, 242 published_at: publishedAt, created_at: now, fan_only: fanOnly, nsfw, content_warning: cw, 242 243 }).catch(() => { /* best-effort */ }); 243 244 } … … 298 299 const fanOnly = req.body.fan_only ? 1 : 0; 299 300 const nsfw = req.body.nsfw ? 1 : 0; 301 const cw = (req.body.content_warning || '').trim().slice(0, 200); 300 302 const newSlug = req.body.slug; 301 303 const action = req.body.action || 'save'; … … 336 338 title = ?, content = ?, excerpt = ?, status = ?, 337 339 cover_image_url = ?, pinned = ?, tags = ?, 338 type = ?, noindex = ?, fan_only = ?, nsfw = ?, publish_at = ?,340 type = ?, noindex = ?, fan_only = ?, nsfw = ?, content_warning = ?, publish_at = ?, 339 341 slug = ?, published_at = ?, updated_at = ? 340 342 WHERE id = ? … … 343 345 cover_image_url || null, parsePinnedRank(pinned), 344 346 JSON.stringify((tags || '').split(',').map(t => t.trim()).filter(Boolean)), 345 finalType, noindex ? 1 : 0, fanOnly, nsfw, publishAt,347 finalType, noindex ? 1 : 0, fanOnly, nsfw, cw, publishAt, 346 348 finalSlug, publishedAt, now, post.id 347 349 ); … … 364 366 id: post.id, slug: finalSlug, title: title || finalSlug, 365 367 content: cleanContent, cover_image_url: cover_image_url || null, 366 published_at: publishedAt, created_at: post.created_at, fan_only: fanOnly, nsfw, 368 published_at: publishedAt, created_at: post.created_at, fan_only: fanOnly, nsfw, content_warning: cw, 367 369 }; 368 370 if (post.status !== 'published') ActivityPubService.deliverCreate(site, apPost).catch(() => { /* best-effort */ }); -
src/services/ActivityPubService.js
r92f6976 rb7d4458 250 250 sensitive: !!post.nsfw, 251 251 }; 252 if (post.nsfw) note.summary = 'Gevoelige inhoud';252 if (post.nsfw) note.summary = post.content_warning || 'Gevoelige inhoud'; 253 253 if (attachment.length) note.attachment = attachment; 254 254 // Playable-audio posts suppress the cover attachment (player card). Still expose … … 708 708 // the timeline). 709 709 for (const s of subs) { 710 tlStmts().ins.run(o.id, s.slug, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, o.url || null, o.published || null, media );710 tlStmts().ins.run(o.id, s.slug, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, o.url || null, o.published || null, media, o.sensitive ? 1 : 0, o.summary || null); 711 711 } 712 712 console.log('[AP] timeline +', actorUri, 'x' + subs.length); … … 775 775 if (!site) return; 776 776 const recent = db.prepare( 777 `SELECT id, slug, title, content, cover_image_url, nsfw, published_at, created_at777 `SELECT id, slug, title, content, cover_image_url, nsfw, content_warning, published_at, created_at 778 778 FROM posts WHERE site_id = ? AND status = 'published' AND (fan_only IS NULL OR fan_only = 0) 779 779 ORDER BY COALESCE(published_at, created_at) DESC LIMIT 20` … … 1011 1011 url: note.url || url, 1012 1012 content: HtmlSanitizerService.sanitize(rawHtml), // full, sanitized 1013 sensitive: !!note.sensitive, // remote CW → blur in the Cirkel 1014 cw: note.summary || '', 1013 1015 images, 1014 1016 threadInboxes, // every ancestor author's inbox … … 1083 1085 function tlStmts() { 1084 1086 if (!_insTl) { 1085 _insTl = db.prepare('INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, author_name, author_handle, author_icon, author_url, content, url, published, media_json, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)');1087 _insTl = db.prepare('INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, author_name, author_handle, author_icon, author_url, content, url, published, media_json, nsfw, cw, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)'); 1086 1088 _listTl = db.prepare('SELECT * FROM ap_timeline WHERE slug = ? ORDER BY COALESCE(published, created_at) DESC LIMIT ?'); 1087 1089 _delTl = db.prepare('DELETE FROM ap_timeline WHERE id = ?'); … … 1102 1104 if (!_cirkelPosts) _cirkelPosts = db.prepare(` 1103 1105 SELECT t.id, t.author_uri, t.author_name, t.author_handle, t.author_icon, t.author_url, 1104 t.content, t.url, t.published, t.media_json, t.boosted 1106 t.content, t.url, t.published, t.media_json, t.boosted, t.nsfw, t.cw 1105 1107 FROM ap_timeline t 1106 1108 LEFT JOIN ap_following f ON f.slug = t.slug AND f.actor_uri = t.author_uri … … 1142 1144 tlStmts().ins.run(id, slug, note.actor_uri || '', note.actor_name || '', note.actor_handle || '', 1143 1145 note.actor_icon || '', note.actor_url || '', note.content || '', note.url || null, 1144 new Date().toISOString(), media );1146 new Date().toISOString(), media, note.sensitive ? 1 : 0, note.cw || null); 1145 1147 } catch { /* ignore */ } 1146 1148 markBoosted(slug, id); -
src/services/Scheduler.js
r92f6976 rb7d4458 15 15 try { 16 16 const due = db.prepare(` 17 SELECT p.id, p.site_id, p.slug, p.title, p.content, p.cover_image_url, p.fan_only, p.nsfw, 17 SELECT p.id, p.site_id, p.slug, p.title, p.content, p.cover_image_url, p.fan_only, p.nsfw, p.content_warning, 18 18 p.published_at, p.publish_at, p.created_at, u.username 19 19 FROM posts p JOIN users u ON u.id = p.author_id … … 39 39 id: p.id, slug: p.slug, title: p.title || p.slug, 40 40 content: p.content, cover_image_url: p.cover_image_url || null, 41 published_at: p.published_at || p.publish_at, created_at: p.created_at, fan_only: p.fan_only, nsfw: p.nsfw, 41 published_at: p.published_at || p.publish_at, created_at: p.created_at, fan_only: p.fan_only, nsfw: p.nsfw, content_warning: p.content_warning, 42 42 }).catch(() => { /* best-effort */ }); 43 43 } -
src/services/i18n.js
r92f6976 rb7d4458 771 771 'pedit.pin_top': 'bovenaan', 772 772 'pedit.pin_nth_suffix': 'e van boven', 773 'pedit.noindex_label': 'noindex (verberg voor zoekmachines)', 'pedit.nsfw_label': 'NSFW / gevoelige inhoud', 'p ost.nsfw_warning': 'Gevoelige inhoud', 'post.nsfw_show': 'Tonen',773 'pedit.noindex_label': 'noindex (verberg voor zoekmachines)', 'pedit.nsfw_label': 'NSFW / gevoelige inhoud', 'pedit.nsfw_cw_ph': 'Waarschuwingstekst (optioneel, standaard: Gevoelige inhoud)', 'post.nsfw_warning': 'Gevoelige inhoud', 'post.nsfw_show': 'Tonen', 774 774 'pedit.fan_only_label': 'Alleen voor ingelogde fans', 775 775 'pedit.schedule_label': 'Publicatie inplannen', … … 1782 1782 'pedit.pin_top': 'at the top', 1783 1783 'pedit.pin_nth_suffix': 'th from top', 1784 'pedit.noindex_label': 'noindex (hide from search engines)', 'pedit.nsfw_label': 'NSFW / sensitive content', 'p ost.nsfw_warning': 'Sensitive content', 'post.nsfw_show': 'Show',1784 'pedit.noindex_label': 'noindex (hide from search engines)', 'pedit.nsfw_label': 'NSFW / sensitive content', 'pedit.nsfw_cw_ph': 'Warning text (optional, default: Sensitive content)', 'post.nsfw_warning': 'Sensitive content', 'post.nsfw_show': 'Show', 1785 1785 'pedit.fan_only_label': 'Logged-in fans only', 1786 1786 'pedit.schedule_label': 'Schedule publication', … … 2793 2793 'pedit.pin_top': 'ganz oben', 2794 2794 'pedit.pin_nth_suffix': '. von oben', 2795 'pedit.noindex_label': 'noindex (vor Suchmaschinen verbergen)', 'pedit.nsfw_label': 'NSFW / sensibler Inhalt', 'p ost.nsfw_warning': 'Sensibler Inhalt', 'post.nsfw_show': 'Anzeigen',2795 'pedit.noindex_label': 'noindex (vor Suchmaschinen verbergen)', 'pedit.nsfw_label': 'NSFW / sensibler Inhalt', 'pedit.nsfw_cw_ph': 'Warntext (optional, Standard: Sensibler Inhalt)', 'post.nsfw_warning': 'Sensibler Inhalt', 'post.nsfw_show': 'Anzeigen', 2796 2796 'pedit.fan_only_label': 'Nur für angemeldete Fans', 2797 2797 'pedit.schedule_label': 'Veröffentlichung planen', -
src/views/pages/post-edit.ejs
r92f6976 rb7d4458 242 242 <span>🔞 <%= t('pedit.nsfw_label') %></span> 243 243 </label> 244 <input type="text" name="content_warning" value="<%= post.content_warning || '' %>" maxlength="200" 245 placeholder="<%= t('pedit.nsfw_cw_ph') %>" 246 style="width:100%;box-sizing:border-box;margin:6px 0 2px;font-size:13px;padding:7px 9px"> 244 247 <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %> 245 248 <label class="pe-checkbox"> -
src/views/pages/post.ejs
r92f6976 rb7d4458 13 13 <div class="nsfw-banner"> 14 14 <span class="nsfw-banner-icon">🔞</span> 15 <span class="nsfw-banner-text"><%= t('post.nsfw_warning') %></span>15 <span class="nsfw-banner-text"><%= post.content_warning || t('post.nsfw_warning') %></span> 16 16 <button type="button" class="nsfw-banner-btn nsfw-reveal"><%= t('post.nsfw_show') %></button> 17 17 </div> -
src/views/partials/post-card.ejs
r92f6976 rb7d4458 50 50 aria-label="<%= post.title || '(zonder titel)' %>" tabindex="-1"> 51 51 <img src="<%= post.cover_image_url %>" alt="" loading="lazy" decoding="async"> 52 <% if (post.nsfw) { %><span class="nsfw-veil"><span class="nsfw-veil-label">🔞 <%= t('post.nsfw_warning') %></span><span class="nsfw-veil-btn"><%= t('post.nsfw_show') %></span></span><% } %>52 <% if (post.nsfw) { %><span class="nsfw-veil"><span class="nsfw-veil-label">🔞 <%= post.content_warning || t('post.nsfw_warning') %></span><span class="nsfw-veil-btn"><%= t('post.nsfw_show') %></span></span><% } %> 53 53 </a> 54 54 <% } %> -
src/views/partials/post-tile.ejs
r92f6976 rb7d4458 37 37 <% if (_src) { %><span class="grid-tile-gradient-src">van <%= _src %></span><% } %> 38 38 <% } %> 39 <% if (post.nsfw) { %><div class="nsfw-veil"><span class="nsfw-veil-label">🔞 <%= t('post.nsfw_warning') %></span><span class="nsfw-veil-btn"><%= t('post.nsfw_show') %></span></div><% } %>39 <% if (post.nsfw) { %><div class="nsfw-veil"><span class="nsfw-veil-label">🔞 <%= post.content_warning || t('post.nsfw_warning') %></span><span class="nsfw-veil-btn"><%= t('post.nsfw_show') %></span></div><% } %> 40 40 </a>
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)