Changeset 59f0170 in Klonkt
- Timestamp:
- 06/24/2026 02:26:32 PM (3 months ago)
- Branches:
- main
- Children:
- dc8e9bd
- Parents:
- 2e9773f
- Location:
- src
- Files:
-
- 3 deleted
- 6 edited
-
routes/admin-comments.js (deleted)
-
routes/comments.js (deleted)
-
routes/posts.js (modified) (2 diffs)
-
server.js (modified) (3 diffs)
-
views/pages/admin-comments.ejs (deleted)
-
views/pages/admin-site-edit.ejs (modified) (1 diff)
-
views/pages/admin.ejs (modified) (2 diffs)
-
views/pages/my-site.ejs (modified) (1 diff)
-
views/pages/post.ejs (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/posts.js
r2e9773f r59f0170 710 710 } 711 711 712 // Comments: top-level + replies. Two-pass build: fetch all approved 713 // comments for the post, then group replies under their parent. 714 const commentRows = db.prepare(` 715 SELECT c.id, c.parent_comment_id, c.content, c.status, c.created_at, 716 c.author_id, u.username AS author_username, 717 COALESCE(u.avatar_url, (SELECT profile_photo FROM sites WHERE owner_id = u.id AND profile_photo IS NOT NULL ORDER BY is_primary DESC, created_at ASC LIMIT 1)) AS author_avatar 718 FROM comments c JOIN users u ON u.id = c.author_id 719 WHERE c.post_id = ? AND c.status = 'approved' 720 ORDER BY c.created_at ASC 721 `).all(post.id); 722 const topLevel = []; 723 const repliesById = new Map(); 724 for (const c of commentRows) { 725 if (c.parent_comment_id) { 726 if (!repliesById.has(c.parent_comment_id)) repliesById.set(c.parent_comment_id, []); 727 repliesById.get(c.parent_comment_id).push(c); 728 } else { 729 topLevel.push(c); 730 } 731 } 732 for (const c of topLevel) c.replies = repliesById.get(c.id) || []; 733 const totalComments = commentRows.length; 712 // Native comments removed: social interaction is fediverse-only (see the 713 // "From the fediverse" section below). 734 714 735 715 // Prev / next chronological (kept for back-compat — "post-nav" feature … … 818 798 olderPost, 819 799 relatedPosts, 820 comments: topLevel,821 totalComments,822 800 fediverse, 823 801 canManageSite, -
src/server.js
r2e9773f r59f0170 32 32 import adminSitesRoutes from './routes/admin-sites.js'; 33 33 import adminUsersRoutes from './routes/admin-users.js'; 34 import adminCommentsRoutes from './routes/admin-comments.js';35 34 import adminSettingsRoutes from './routes/admin-settings.js'; 36 35 import adminSeoRoutes from './routes/admin-seo.js'; 37 36 import audioRoutes from './routes/audio.js'; 38 37 import searchRoutes from './routes/search.js'; 39 import commentsRoutes from './routes/comments.js';40 38 import tagsRoutes from './routes/tags.js'; 41 39 import typesRoutes from './routes/types.js'; … … 305 303 app.use('/admin/sites', adminSitesRoutes); 306 304 app.use('/admin/users', adminUsersRoutes); 307 app.use('/admin/comments', adminCommentsRoutes);308 305 app.use('/admin/settings', adminSettingsRoutes); 309 306 app.use('/admin/seo', adminSeoRoutes); … … 318 315 if (audioEnabled()) app.use('/audio', audioRoutes); 319 316 app.use('/search', searchRoutes); 320 app.use('/comments', commentsRoutes);321 317 app.use('/tag', tagsRoutes); 322 318 app.use('/type', typesRoutes); -
src/views/pages/admin-site-edit.ejs
r2e9773f r59f0170 147 147 <input type="checkbox" name="robots_index" value="1" <%= site.robots_index ? 'checked' : '' %>> 148 148 <span><%= t('asite.robots_index') %></span> 149 </label>150 <label class="cb">151 <input type="checkbox" name="require_login_to_comment" value="1" <%= site.require_login_to_comment ? 'checked' : '' %>>152 <span><%= t('asite.require_login_comment') %></span>153 </label>154 <label>155 <span><%= t('asite.comment_moderation') %></span>156 <select name="comments_moderation_mode">157 <option value="trust" <%= (site.comments_moderation_mode || 'trust') === 'trust' ? 'selected' : '' %>><%= t('asite.moderation_trust') %></option>158 <option value="moderate" <%= site.comments_moderation_mode === 'moderate' ? 'selected' : '' %>><%= t('asite.moderation_moderate') %></option>159 </select>160 149 </label> 161 150 <% if (typeof audioEnabled === 'undefined' || audioEnabled) { %> -
src/views/pages/admin.ejs
r2e9773f r59f0170 31 31 <a href="/admin/playlists" class="btn"><%= t('admin.b_playlists') %></a> 32 32 <% } %> 33 <a href="/admin/comments" class="btn"><%= t('admin.b_comments') %></a>34 33 <% if (primarySite) { %><a href="/admin/seo" class="btn"><%= t('admin.b_seo') %></a><% } %> 35 34 <a href="/admin/settings" class="btn"><%= t('admin.b_settings') %></a> … … 39 38 <a href="/admin/audio" class="btn"><%= t('admin.b_audio') %></a> 40 39 <% } %> 41 <a href="/admin/comments" class="btn"><%= t('admin.b_comments') %></a>42 40 <% if (primarySite) { %> 43 41 <a href="/admin/sites/<%= primarySite.slug %>/edit" class="btn"><%= t('admin.b_look') %></a> -
src/views/pages/my-site.ejs
r2e9773f r59f0170 7 7 <a href="/user/<%= mySite.slug %>/posts/new" class="btn">✍️ <%= t('myst.new_post') %></a> 8 8 <a href="/admin/sites/<%= mySite.slug %>/edit" class="btn">🎨 <%= t('myst.appearance') %></a> 9 <a href="/user/<%= mySite.slug %>/admin/comments" class="btn">💬 <%= t('myst.comments') %></a>10 9 <a href="/user/<%= mySite.slug %>" class="btn">👁️ <%= t('myst.view_site') %></a> 11 10 <a href="/account" class="btn">👤 <%= t('myst.account') %></a> -
src/views/pages/post.ejs
r2e9773f r59f0170 112 112 </script> 113 113 114 <!-- Comments -->115 <section class="post-comments" id="comments">116 <h2 class="comments-heading">117 <%= t(totalComments === 1 ? 'comments.heading_one' : 'comments.heading_other', { n: totalComments }) %>118 </h2>119 120 <% if (typeof currentPath === 'string' && currentPath && currentPath.indexOf('/' + post.slug) === 0) { %>121 <% /* If the URL has ?pending=1, the previous submit landed in the moderation queue */ %>122 <% } %>123 <script>124 (function() {125 if (location.search.indexOf('pending=1') !== -1) {126 var s = document.createElement('div');127 s.className = 'comments-pending-flash';128 s.textContent = <%- JSON.stringify(t('comments.pending')) %>;129 document.currentScript.parentNode.insertBefore(s, document.currentScript);130 }131 })();132 </script>133 134 <% if (!comments || !comments.length) { %>135 <p class="comments-empty"><%= t('comments.empty') %><% if (!user) { %> <a href="/auth/login?next=<%= encodeURIComponent(_base + '/' + post.slug + '#comments') %>"><%= t('nav.login') %></a> <%= t('comments.to_start') %><% } %></p>136 <% } else { %>137 <ol class="comments-list">138 <% comments.forEach(function(c) { %>139 <li class="comment" id="comment-<%= c.id %>">140 <div class="comment-avatar">141 <% if (c.author_avatar) { %>142 <img src="<%= c.author_avatar %>" alt="">143 <% } else { %>144 <span><%= c.author_username.charAt(0).toUpperCase() %></span>145 <% } %>146 </div>147 <div class="comment-body">148 <div class="comment-meta">149 <a class="comment-author" href="/users/<%= encodeURIComponent(c.author_username) %>"><%= c.author_username %></a>150 <span class="comment-time" title="<%= c.created_at %>"><%= formatDateTime(c.created_at) %></span>151 </div>152 <div class="comment-content"><%= c.content %></div>153 <div class="comment-actions">154 <% if (user && canMutate) { %>155 <button type="button" class="comment-reply-btn" data-reply-to="<%= c.id %>"><%= t('comments.reply') %></button>156 <% } %>157 <% if (user && permissions.canDeleteComment(user, c, site)) { %>158 <form method="post" action="<%= _base %>/comments/<%= c.id %>/delete" onsubmit="return confirm('<%= t('comments.delete_confirm') %>')">159 <button type="submit" class="comment-delete-btn"><%= t('comments.delete') %></button>160 </form>161 <% } %>162 </div>163 164 <!-- Inline reply form (hidden by default) -->165 <% if (user && canMutate) { %>166 <form method="post" action="<%= _base %>/comments" class="comment-reply-form" hidden data-reply-form-for="<%= c.id %>">167 <input type="hidden" name="post_slug" value="<%= post.slug %>">168 <input type="hidden" name="parent_comment_id" value="<%= c.id %>">169 <textarea name="content" rows="3" maxlength="4000" placeholder="<%= t('comments.reply_to', { name: c.author_username }) %>" required></textarea>170 <div class="comment-reply-form-actions">171 <button type="submit" class="btn btn-primary"><%= t('comments.reply') %></button>172 <button type="button" class="btn comment-cancel-reply"><%= t('comments.cancel') %></button>173 </div>174 </form>175 <% } %>176 177 <!-- Replies (1 level deep) -->178 <% if (c.replies && c.replies.length) { %>179 <ol class="comment-replies">180 <% c.replies.forEach(function(r) { %>181 <li class="comment comment-reply" id="comment-<%= r.id %>">182 <div class="comment-avatar">183 <% if (r.author_avatar) { %>184 <img src="<%= r.author_avatar %>" alt="">185 <% } else { %>186 <span><%= r.author_username.charAt(0).toUpperCase() %></span>187 <% } %>188 </div>189 <div class="comment-body">190 <div class="comment-meta">191 <a class="comment-author" href="/users/<%= encodeURIComponent(r.author_username) %>"><%= r.author_username %></a>192 <span class="comment-time"><%= formatDateTime(r.created_at) %></span>193 </div>194 <div class="comment-content"><%= r.content %></div>195 <div class="comment-actions">196 <% if (user && canMutate) { %>197 <button type="button" class="comment-reply-btn" data-reply-to="<%= r.id %>"><%= t('comments.reply') %></button>198 <% } %>199 <% if (user && permissions.canDeleteComment(user, r, site)) { %>200 <form method="post" action="<%= _base %>/comments/<%= r.id %>/delete" onsubmit="return confirm('<%= t('comments.delete_confirm') %>')">201 <button type="submit" class="comment-delete-btn"><%= t('comments.delete') %></button>202 </form>203 <% } %>204 </div>205 206 <%# Replying to a reply — lands (via resolvedParent on the207 server) in the same thread, so the 1-level depth is preserved. %>208 <% if (user && canMutate) { %>209 <form method="post" action="<%= _base %>/comments" class="comment-reply-form" hidden data-reply-form-for="<%= r.id %>">210 <input type="hidden" name="post_slug" value="<%= post.slug %>">211 <input type="hidden" name="parent_comment_id" value="<%= r.id %>">212 <textarea name="content" rows="3" maxlength="4000" placeholder="<%= t('comments.reply_to', { name: r.author_username }) %>" required></textarea>213 <div class="comment-reply-form-actions">214 <button type="submit" class="btn btn-primary"><%= t('comments.reply') %></button>215 <button type="button" class="btn comment-cancel-reply"><%= t('comments.cancel') %></button>216 </div>217 </form>218 <% } %>219 </div>220 </li>221 <% }); %>222 </ol>223 <% } %>224 </div>225 </li>226 <% }); %>227 </ol>228 <% } %>229 230 <!-- Top-level comment form (only for logged-in users for now) -->231 <% if (user && canMutate) { %>232 <form method="post" action="<%= _base %>/comments" class="comment-form">233 <input type="hidden" name="post_slug" value="<%= post.slug %>">234 <label class="comment-form-label">235 <span><%= t('comments.add_as') %> <strong><%= user.username %></strong></span>236 <textarea name="content" rows="3" maxlength="4000" placeholder="<%= t('comments.placeholder') %>" required></textarea>237 </label>238 <button type="submit" class="btn btn-primary"><%= t('comments.post') %></button>239 </form>240 <% } else if (!user) { %>241 <p class="comments-login-cta">242 <a href="/auth/login?next=<%= encodeURIComponent(_base + '/' + post.slug + '#comments') %>" class="btn"><%= t('comments.login_to_comment') %></a>243 </p>244 <% } else { %>245 <p class="comments-login-cta" style="color:var(--ink-muted)">👁️ Kijker-modus — reageren is uitgeschakeld.</p>246 <% } %>247 </section>248 249 <!-- Reply form toggle -->250 <script>251 (function() {252 document.querySelectorAll('.comment-reply-btn').forEach(function(btn) {253 btn.addEventListener('click', function() {254 var id = btn.dataset.replyTo;255 var form = document.querySelector('[data-reply-form-for="' + id + '"]');256 if (form) {257 form.hidden = !form.hidden;258 if (!form.hidden) form.querySelector('textarea').focus();259 }260 });261 });262 document.querySelectorAll('.comment-cancel-reply').forEach(function(btn) {263 btn.addEventListener('click', function() {264 btn.closest('.comment-reply-form').hidden = true;265 });266 });267 })();268 </script>269 114 270 115 <%# ── Related posts (3-card grid) ─────────────────────────────
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)