|<\/p>/g, '');
@@ -264,35 +387,7 @@ class UserCommentSystem {
const fullDate = new Date(c.created_at).toISOString();
const content = this.renderCommentContent(c.content, c.item_id);
- // Replicating the structure of comments.js but adapting for the list view
- // We add a header indicating which item this comment belongs to
-
- return `
-
- `;
+ return ``;
}
startLiveTimestamps() {
@@ -336,15 +431,21 @@ class UserCommentSystem {
return div.innerHTML;
}
}
+}
// Initializer for AJAX and standard load
window.initUserComments = () => {
- // Prevent multiple instances if already running on this container
- if (document.getElementById('user-comments-container')) {
+ const container = document.getElementById('user-comments-container');
+ if (container && !container.dataset.initialized) {
+ container.dataset.initialized = 'true';
new UserCommentSystem();
}
};
-window.addEventListener('DOMContentLoaded', () => {
+if (document.readyState === 'loading') {
+ window.addEventListener('DOMContentLoaded', () => {
+ window.initUserComments();
+ });
+} else {
window.initUserComments();
-});
+}
diff --git a/src/comment_upload_handler.mjs b/src/comment_upload_handler.mjs
index 08a9563..8233bb5 100644
--- a/src/comment_upload_handler.mjs
+++ b/src/comment_upload_handler.mjs
@@ -12,7 +12,6 @@ const sendJson = (res, data, code = 200) => {
res.end(JSON.stringify(data));
};
-// One-time migration: ensure comment_files table exists
db`CREATE TABLE IF NOT EXISTS public.comment_files (
id SERIAL PRIMARY KEY,
comment_id INTEGER REFERENCES public.comments(id) ON DELETE CASCADE,
@@ -29,6 +28,8 @@ db`CREATE SEQUENCE IF NOT EXISTS comment_files_id_seq`.catch(() => { });
db`ALTER TABLE comment_files ALTER COLUMN id SET DEFAULT nextval('comment_files_id_seq')`.catch(() => { });
db`CREATE INDEX IF NOT EXISTS idx_comment_files_comment_id ON public.comment_files(comment_id)`.catch(() => { });
db`CREATE INDEX IF NOT EXISTS idx_comment_files_checksum ON public.comment_files(checksum)`.catch(() => { });
+db`ALTER TABLE public.comment_files ADD CONSTRAINT comment_files_pkey PRIMARY KEY (id)`.catch(() => { });
+db`ALTER TABLE public.comment_files REPLICA IDENTITY DEFAULT`.catch(() => { });
/**
* Parse multipart form data supporting multiple files with the same field name.
diff --git a/src/inc/routes/comments.mjs b/src/inc/routes/comments.mjs
index a19bc6d..a331d36 100644
--- a/src/inc/routes/comments.mjs
+++ b/src/inc/routes/comments.mjs
@@ -205,13 +205,122 @@ export default (router, tpl) => {
// Let's modify comments content in-place (or new array) before mapping
const mentionsProcessed = await f0cklib.processMentions(comments);
- const processedComments = mentionsProcessed.map(c => {
+ let processedComments = mentionsProcessed.map(c => {
return {
...c,
content: c.content
};
});
+ // Fetch file attachments for all fetched comments
+ if (processedComments.length > 0) {
+ const commentIds = processedComments.map(c => c.id);
+ try {
+ const files = await db`
+ SELECT id, comment_id, dest, mime, size, original_filename
+ FROM comment_files
+ WHERE comment_id = ANY(${commentIds}::int[])
+ ORDER BY id ASC
+ `;
+ const filesMap = new Map();
+ for (const f of files) {
+ if (!filesMap.has(f.comment_id)) filesMap.set(f.comment_id, []);
+ filesMap.get(f.comment_id).push(f);
+ }
+ for (const c of processedComments) {
+ c.files = filesMap.get(c.id) || [];
+ }
+ } catch (e) {
+ for (const c of processedComments) c.files = [];
+ }
+
+ // Fetch poll data for comments
+ if (cfg.websrv.enable_comment_polls) {
+ try {
+ const commentIds = processedComments.map(c => c.id);
+ const pollRows = await db`
+ SELECT
+ cp.id as poll_id,
+ cp.comment_id,
+ cp.question,
+ cp.expires_at,
+ COALESCE(cp.is_anonymous, true) as is_anonymous,
+ json_agg(
+ json_build_object(
+ 'id', cpo.id,
+ 'text', cpo.text,
+ 'sort_order', cpo.sort_order,
+ 'vote_count', COALESCE(vote_counts.cnt, 0)
+ ) ORDER BY cpo.sort_order ASC, cpo.id ASC
+ ) AS options,
+ COALESCE(SUM(vote_counts.cnt), 0)::int AS total_votes
+ FROM comment_polls cp
+ JOIN comment_poll_options cpo ON cpo.poll_id = cp.id
+ LEFT JOIN (
+ SELECT option_id, COUNT(*) AS cnt
+ FROM comment_poll_votes
+ GROUP BY option_id
+ ) vote_counts ON vote_counts.option_id = cpo.id
+ WHERE cp.comment_id = ANY(${commentIds}::int[])
+ GROUP BY cp.id, cp.comment_id, cp.question, cp.expires_at, cp.is_anonymous
+ `;
+ // For non-anonymous polls, fetch voter names
+ const nonAnonIds = pollRows.filter(p => !p.is_anonymous).map(p => p.poll_id);
+ let votersByOption = new Map();
+ if (nonAnonIds.length > 0) {
+ const voterRows = await db`
+ SELECT cpv.option_id, u."user" as username, uo.avatar, uo.avatar_file
+ FROM comment_poll_votes cpv
+ JOIN public."user" u ON u.id = cpv.user_id
+ LEFT JOIN public.user_options uo ON uo.user_id = cpv.user_id
+ WHERE cpv.poll_id = ANY(${nonAnonIds}::int[])
+ `;
+ for (const v of voterRows) {
+ if (!votersByOption.has(v.option_id)) votersByOption.set(v.option_id, []);
+ votersByOption.get(v.option_id).push({ username: v.username, avatar: v.avatar, avatar_file: v.avatar_file });
+ }
+ }
+ const pollMap = new Map();
+ for (const p of pollRows) {
+ const options = p.is_anonymous
+ ? p.options
+ : p.options.map(o => ({ ...o, voters: votersByOption.get(o.id) || [] }));
+ pollMap.set(p.comment_id, {
+ id: p.poll_id,
+ question: p.question,
+ expires_at: p.expires_at,
+ is_anonymous: p.is_anonymous,
+ options,
+ total_votes: parseInt(p.total_votes) || 0,
+ user_vote_option_id: null
+ });
+ }
+ // Fill in per-user poll votes if logged in
+ if (req.session && pollRows.length > 0) {
+ const pollIds = pollRows.map(p => p.poll_id);
+ try {
+ const votes = await db`
+ SELECT poll_id, option_id FROM comment_poll_votes
+ WHERE poll_id = ANY(${pollIds}::int[]) AND user_id = ${req.session.id}
+ `;
+ const voteMap = new Map(votes.map(v => [v.poll_id, v.option_id]));
+ for (const [comment_id, poll] of pollMap.entries()) {
+ poll.user_vote_option_id = voteMap.get(poll.id) || null;
+ }
+ } catch (e) { /* graceful */ }
+ }
+ for (const c of processedComments) {
+ c.poll = pollMap.get(c.id) || null;
+ }
+ } catch (e) {
+ console.error('[USER_COMMENTS] Poll fetch error:', e.message);
+ for (const c of processedComments) c.poll = null;
+ }
+ } else {
+ for (const c of processedComments) c.poll = null;
+ }
+ }
+
if (isJson) {
return res.reply({
headers: { 'Content-Type': 'application/json; charset=utf-8' },
diff --git a/views/comments_user-partial.html b/views/comments_user-partial.html
index ce910b2..acc8fc4 100644
--- a/views/comments_user-partial.html
+++ b/views/comments_user-partial.html
@@ -25,4 +25,4 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/views/comments_user.html b/views/comments_user.html
index 0dc07d1..f9f0704 100644
--- a/views/comments_user.html
+++ b/views/comments_user.html
@@ -1,7 +1,9 @@
@include(snippets/header)
+
@include(comments_user-partial)
+
@include(snippets/footer)
\ No newline at end of file