feat: Implement comments, notifications, and custom emojis with new API routes, UI components, and database migrations.

This commit is contained in:
x
2026-01-25 03:48:24 +01:00
parent 595118c2c8
commit d903ce8b98
18 changed files with 1900 additions and 44 deletions

186
src/inc/routes/comments.mjs Normal file
View File

@@ -0,0 +1,186 @@
import db from "../sql.mjs";
import f0cklib from "../routeinc/f0cklib.mjs"; // Assuming this exists or we need to check imports
export default (router, tpl) => {
// Fetch comments for an item
router.get(/\/api\/comments\/(?<itemid>\d+)/, async (req, res) => {
const itemId = req.params.itemid;
const sort = req.url.qs?.sort || 'new'; // 'new' or 'old'
try {
const comments = await db`
SELECT
c.id, c.parent_id, c.content, c.created_at, c.vote_score, c.is_deleted,
u.user as username, u.id as user_id, uo.avatar,
(SELECT count(*) FROM comments r WHERE r.parent_id = c.id) as reply_count
FROM comments c
JOIN "user" u ON c.user_id = u.id
LEFT JOIN user_options uo ON uo.user_id = u.id
WHERE c.item_id = ${itemId}
ORDER BY c.created_at ${db.unsafe(sort === 'new' ? 'DESC' : 'ASC')}
`;
let is_subscribed = false;
if (req.session) {
const sub = await db`SELECT 1 FROM comment_subscriptions WHERE user_id = ${req.session.id} AND item_id = ${itemId}`;
if (sub.length > 0) is_subscribed = true;
}
// Transform for frontend if needed, or send as is
return res.reply({
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
success: true,
comments,
is_subscribed,
user_id: req.session ? req.session.user : null,
is_admin: req.session ? req.session.admin : false
})
})
} catch (err) {
console.error(err);
return res.reply({
code: 500,
body: JSON.stringify({ success: false, message: "Database error" })
});
}
});
// Post a comment
router.post('/api/comments', async (req, res) => {
if (!req.session) return res.reply({ code: 401, body: JSON.stringify({ success: false, message: "Unauthorized" }) });
console.log("DEBUG: POST /api/comments");
// Use standard framework parsing
const body = req.post || {};
const item_id = parseInt(body.item_id, 10);
const parent_id = body.parent_id ? parseInt(body.parent_id, 10) : null;
const content = body.content;
console.log("DEBUG: Posting comment:", { item_id, parent_id, content: content?.substring(0, 20) });
if (!content || !content.trim()) {
return res.reply({ body: JSON.stringify({ success: false, message: "Empty comment" }) });
}
try {
const newComment = await db`
INSERT INTO comments ${db({
item_id,
user_id: req.session.id,
parent_id: parent_id || null,
content: content
})}
RETURNING id, created_at
`;
const commentId = parseInt(newComment[0].id, 10);
// Notify Subscribers (excluding the author)
// 1. Get subscribers of the item
// 2. If it's a reply, notify parent author? (Optional, complex logic. Let's stick to item subscription for now + Parent author)
// Logic: Notify users who subscribed to this item OR are the parent author.
// Exclude current user.
// 1. Get subscribers
const subscribers = await db`SELECT user_id FROM comment_subscriptions WHERE item_id = ${item_id}`;
// 2. Get parent author
let parentAuthor = [];
if (parent_id) {
parentAuthor = await db`SELECT user_id FROM comments WHERE id = ${parent_id}`;
}
// 3. Collect unique recipients
const recipients = new Set();
subscribers.forEach(s => recipients.add(s.user_id));
parentAuthor.forEach(p => recipients.add(p.user_id));
// Remove self
recipients.delete(req.session.id);
// 4. Batch insert
if (recipients.size > 0) {
const notificationsToAdd = Array.from(recipients).map(uid => ({
user_id: uid,
type: 'comment_reply',
item_id: item_id, // Already parsed as int
comment_id: commentId, // Already parsed as int
reference_id: commentId // Already parsed as int
}));
await db`INSERT INTO notifications ${db(notificationsToAdd)}`;
}
return res.reply({
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ success: true, comment: newComment[0] })
});
} catch (err) {
console.error(err);
return res.reply({
code: 500,
body: JSON.stringify({ success: false, message: "Database error" })
});
}
});
// Subscribe toggle
router.post(/\/api\/subscribe\/(?<itemid>\d+)/, async (req, res) => {
if (!req.session) return res.reply({ code: 401, body: JSON.stringify({ success: false }) });
const itemId = req.params.itemid;
try {
const existing = await db`
SELECT 1 FROM comment_subscriptions
WHERE user_id = ${req.session.id} AND item_id = ${itemId}
`;
let subscribed = false;
if (existing.length > 0) {
await db`DELETE FROM comment_subscriptions WHERE user_id = ${req.session.id} AND item_id = ${itemId}`;
} else {
await db`INSERT INTO comment_subscriptions (user_id, item_id) VALUES (${req.session.id}, ${itemId})`;
subscribed = true;
}
return res.reply({
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ success: true, subscribed })
});
} catch (e) {
return res.reply({ code: 500, body: JSON.stringify({ success: false }) });
}
});
// Delete comment
router.post(/\/api\/comments\/(?<id>\d+)\/delete/, async (req, res) => {
if (!req.session) return res.reply({ code: 401, body: JSON.stringify({ success: false }) });
const commentId = req.params.id;
try {
const comment = await db`SELECT user_id FROM comments WHERE id = ${commentId}`;
if (!comment.length) return res.reply({ code: 404, body: JSON.stringify({ success: false, message: "Not found" }) });
if (!req.session.admin && comment[0].user_id !== req.session.id) {
return res.reply({ code: 403, body: JSON.stringify({ success: false, message: "Forbidden" }) });
}
await db`UPDATE comments SET is_deleted = true, content = '[deleted]' WHERE id = ${commentId}`;
return res.reply({
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ success: true })
});
} catch (e) {
return res.reply({ code: 500, body: JSON.stringify({ success: false }) });
}
});
return router;
};