Changeset c9c6a2d in Klonkt
- Timestamp:
- 06/21/2026 06:38:24 PM (3 months ago)
- Branches:
- main
- Children:
- 9a34d31
- Parents:
- d41f4be
- Location:
- src
- Files:
-
- 3 added
- 9 edited
-
config/database.js (modified) (1 diff)
-
middleware/render.js (modified) (2 diffs)
-
routes/comments.js (modified) (4 diffs)
-
routes/notifications.js (added)
-
routes/posts.js (modified) (3 diffs)
-
server.js (modified) (2 diffs)
-
services/NotificationService.js (added)
-
services/i18n.js (modified) (3 diffs)
-
views/pages/notifications.ejs (added)
-
views/partials/bottom-tab.ejs (modified) (1 diff)
-
views/partials/profile-sheet.ejs (modified) (1 diff)
-
views/partials/topnav.ejs (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/config/database.js
rd41f4be rc9c6a2d 252 252 `); 253 253 254 // Meldingen: iemand reageert op je reactie / post, of liket je post. Snapshots 255 // van naam/titel zodat de lijst goedkoop te tonen is zonder joins. 256 db.exec(` 257 CREATE TABLE IF NOT EXISTS notifications ( 258 id TEXT PRIMARY KEY, 259 user_id TEXT NOT NULL, 260 type TEXT NOT NULL, 261 actor_id TEXT, 262 actor_name TEXT, 263 post_slug TEXT, 264 post_title TEXT, 265 url TEXT, 266 read INTEGER DEFAULT 0, 267 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 268 ); 269 CREATE INDEX IF NOT EXISTS idx_notif_user ON notifications(user_id, read, created_at); 270 `); 271 254 272 // Link-in-bio klikstatistiek (premium #6). Per (site, url) een teller; de 255 273 // link-in-bio-pagina linkt via /links/go/:i dat de klik telt en doorstuurt. -
src/middleware/render.js
rd41f4be rc9c6a2d 19 19 import { getSetting } from '../services/SettingsService.js'; 20 20 import { isPremium as isPremiumInstance, premiumEnabled, premiumUnlocked } from '../services/PatreonService.js'; 21 import { unreadCount as notifUnreadCount } from '../services/NotificationService.js'; 21 22 import { PLATFORMS as PLATFORMS_CATALOG } from '../services/PlatformIcons.js'; 22 23 import { t as i18nT, resolveLang, SUPPORTED as LANGS, LANG_NAMES } from '../services/i18n.js'; … … 94 95 langs: LANGS.map((c) => ({ code: c, name: LANG_NAMES[c], active: c === _lang })), 95 96 timezone: getSetting('timezone') || '', 97 notifUnread: _u ? notifUnreadCount(_u.id) : 0, 96 98 userOwnsSite, 97 99 canSeeBeheer, -
src/routes/comments.js
rd41f4be rc9c6a2d 19 19 import { requireAuth } from '../middleware/auth.js'; 20 20 import PermissionsService from '../services/PermissionsService.js'; 21 import { notify } from '../services/NotificationService.js'; 21 22 22 23 const router = express.Router(); … … 39 40 40 41 const post = db.prepare( 41 'SELECT id, slug FROM posts WHERE site_id = ? AND slug = ? AND status = ?'42 'SELECT id, slug, title, author_id FROM posts WHERE site_id = ? AND slug = ? AND status = ?' 42 43 ).get(site.id, postSlug, 'published'); 43 44 if (!post) return res.status(404).send('Post not found'); … … 50 51 // up to the top-level parent so we never go deeper than 1) 51 52 let resolvedParent = null; 53 let parentAuthorId = null; 52 54 if (parentId) { 53 55 const parent = db.prepare( 54 'SELECT id, parent_comment_id FROM comments WHERE id = ? AND post_id = ?'56 'SELECT id, parent_comment_id, author_id FROM comments WHERE id = ? AND post_id = ?' 55 57 ).get(parentId, post.id); 56 58 if (!parent) return res.status(400).send('Invalid parent comment'); 57 59 resolvedParent = parent.parent_comment_id || parent.id; 60 parentAuthorId = parent.author_id; // ontvanger van de "antwoord"-melding 58 61 } 59 62 … … 73 76 VALUES (?, ?, ?, ?, ?, ?) 74 77 `).run(commentId, post.id, req.session.user.id, resolvedParent, rawContent, status); 78 79 // Melding (alleen bij een zichtbare reactie): antwoord → de auteur van de reactie 80 // waarop gereageerd is; top-level reactie → de auteur van de post. notify() slaat 81 // jezelf-notificeren over. 82 if (status === 'approved') { 83 const url = `${res.locals.siteUrlBase || ''}/${post.slug}#comment-${commentId}`; 84 const actorId = req.session.user.id; 85 const actorName = req.session.user.username; 86 if (parentId) { 87 notify({ userId: parentAuthorId, actorId, actorName, type: 'reply', postSlug: post.slug, postTitle: post.title, url }); 88 } else { 89 notify({ userId: post.author_id, actorId, actorName, type: 'comment', postSlug: post.slug, postTitle: post.title, url }); 90 } 91 } 75 92 76 93 // Where to land after submit: -
src/routes/posts.js
rd41f4be rc9c6a2d 10 10 import { renderPage } from '../middleware/render.js'; 11 11 import { recordPageview, recordPostView } from '../services/StatsService.js'; 12 import { notify } from '../services/NotificationService.js'; 12 13 import PermissionsService from '../services/PermissionsService.js'; 13 14 import MarkdownService from '../services/MarkdownService.js'; … … 405 406 router.post('/posts/:id/like', requireAuth, (req, res) => { 406 407 const userId = req.session.user.id; 407 const post = db.prepare('SELECT id, status FROM posts WHERE id = ?').get(req.params.id);408 const post = db.prepare('SELECT id, status, slug, title, author_id FROM posts WHERE id = ?').get(req.params.id); 408 409 if (!post) return res.status(404).send('Post niet gevonden'); 409 410 if (post.status !== 'published') return res.status(403).send('Niet beschikbaar'); … … 414 415 } else { 415 416 db.prepare('INSERT OR IGNORE INTO post_likes (post_id, user_id) VALUES (?, ?)').run(post.id, userId); 417 // Melding voor de post-auteur (notify slaat jezelf-liken over). 418 notify({ 419 userId: post.author_id, actorId: userId, actorName: req.session.user.username, type: 'like', 420 postSlug: post.slug, postTitle: post.title, url: (res.locals.siteUrlBase || '') + '/' + post.slug, 421 }); 416 422 } 417 423 const likeCount = db.prepare('SELECT COUNT(*) AS c FROM post_likes WHERE post_id = ?').get(post.id).c; -
src/server.js
rd41f4be rc9c6a2d 27 27 import authRoutes from './routes/auth.js'; 28 28 import accountRoutes from './routes/account.js'; 29 import notificationsRoutes from './routes/notifications.js'; 29 30 import adminRoutes from './routes/admin.js'; 30 31 import adminAudioRoutes from './routes/admin-audio.js'; … … 257 258 app.use('/auth', authRoutes); 258 259 app.use('/account', accountRoutes); 260 app.use('/notifications', notificationsRoutes); 259 261 app.use('/admin/audio', adminAudioRoutes); 260 262 app.use('/admin/playlists', adminPlaylistsRoutes); -
src/services/i18n.js
rd41f4be rc9c6a2d 25 25 'nav.new_post': 'Nieuwe post', 26 26 'nav.language': 'Taal', 27 'nav.notifications': 'Meldingen', 28 'notif.title': 'Meldingen', 'notif.empty': 'Nog geen meldingen.', 'notif.someone': 'Iemand', 29 'notif.reply': '{actor} reageerde op je reactie', 'notif.comment': '{actor} reageerde op je post', 'notif.like': '{actor} vindt je post leuk', 27 30 'switch.agenda': 'Agenda', 28 31 'switch.solo': 'Solo', … … 999 1002 'nav.new_post': 'New post', 1000 1003 'nav.language': 'Language', 1004 'nav.notifications': 'Notifications', 1005 'notif.title': 'Notifications', 'notif.empty': 'No notifications yet.', 'notif.someone': 'Someone', 1006 'notif.reply': '{actor} replied to your comment', 'notif.comment': '{actor} commented on your post', 'notif.like': '{actor} liked your post', 1001 1007 'switch.agenda': 'Agenda', 1002 1008 'switch.solo': 'Solo', … … 1964 1970 'nav.new_post': 'Neuer Beitrag', 1965 1971 'nav.language': 'Sprache', 1972 'nav.notifications': 'Benachrichtigungen', 1973 'notif.title': 'Benachrichtigungen', 'notif.empty': 'Noch keine Benachrichtigungen.', 'notif.someone': 'Jemand', 1974 'notif.reply': '{actor} hat auf deinen Kommentar geantwortet', 'notif.comment': '{actor} hat deinen Beitrag kommentiert', 'notif.like': '{actor} gefällt dein Beitrag', 1966 1975 'switch.agenda': 'Termine', 1967 1976 'switch.solo': 'Solo', -
src/views/partials/bottom-tab.ejs
rd41f4be rc9c6a2d 107 107 <% } %> 108 108 <span class="bottom-tab-label"><%= t('tab.profile') %></span> 109 <% if (typeof notifUnread !== 'undefined' && notifUnread > 0) { %> 110 <span class="bottom-tab-badge" aria-label="<%= notifUnread %> ongelezen meldingen"><%= notifUnread > 99 ? '99+' : notifUnread %></span> 111 <% } %> 109 112 </button> 110 113 <% } else { %> -
src/views/partials/profile-sheet.ejs
rd41f4be rc9c6a2d 53 53 <svg class="psi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> 54 54 <span class="psi-label">Account</span> 55 <svg class="psi-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg> 56 </a> 57 </li> 58 <li role="none"> 59 <a href="/notifications" class="profile-sheet-item" role="menuitem" data-close-sheet> 60 <svg class="psi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg> 61 <span class="psi-label"><%= t('nav.notifications') %><% if (typeof notifUnread !== 'undefined' && notifUnread > 0) { %> (<%= notifUnread %>)<% } %></span> 55 62 <svg class="psi-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg> 56 63 </a> -
src/views/partials/topnav.ejs
rd41f4be rc9c6a2d 103 103 <a class="nav-btn" href="<%= _siteUrlBase %>/prutter" aria-label="Prutter (DMs)" title="Prutter"> 104 104 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg> 105 </a> 106 <% } %> 107 108 <% if (user) { %> 109 <a class="nav-btn nav-notif" href="/notifications" aria-label="<%= t('nav.notifications') %>" title="<%= t('nav.notifications') %>" 110 hx-get="/notifications?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" hx-push-url="/notifications" hx-indicator="#pcms-loading"> 111 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg> 112 <% if (typeof notifUnread !== 'undefined' && notifUnread > 0) { %><span class="notif-badge"><%= notifUnread > 9 ? '9+' : notifUnread %></span><% } %> 105 113 </a> 106 114 <% } %> … … 137 145 <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> 138 146 <span><%= t('nav.account') %></span> 147 </a> 148 <a href="/notifications" role="menuitem" class="udi" 149 hx-get="/notifications?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" 150 hx-push-url="/notifications" hx-indicator="#pcms-loading"> 151 <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg> 152 <span><%= t('nav.notifications') %><% if (typeof notifUnread !== 'undefined' && notifUnread > 0) { %> (<%= notifUnread %>)<% } %></span> 139 153 </a> 140 154 <a href="/favorieten" role="menuitem" class="udi" … … 558 572 .ss-all { display: block; margin-top: .35rem; padding: .55rem .6rem; border-top: 1px solid var(--rule); color: var(--accent); font-weight: 600; text-decoration: none; font-size: .9rem; } 559 573 .ss-all:hover { text-decoration: underline; } 574 /* Meldingen-bel + ongelezen-badge */ 575 .nav-notif { position: relative; } 576 .notif-badge { 577 position: absolute; top: 1px; right: 1px; 578 min-width: 16px; height: 16px; padding: 0 4px; box-sizing: border-box; 579 display: inline-flex; align-items: center; justify-content: center; 580 background: #e0245e; color: #fff; font-size: 10px; font-weight: 700; 581 border-radius: 999px; line-height: 1; pointer-events: none; 582 } 560 583 </style>
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)