From e0e04567688e9c6e0dde2d9d8185642bc67ef26f Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Fri, 29 May 2026 20:49:26 +0200 Subject: [PATCH] gfhgfd --- migrations/f0ckm_schema.sql | 1 + public/s/css/f0ckm.css | 78 +++++++++++++++++++++++++++++++++ public/s/js/comments.js | 42 ++++++++++++++++-- public/s/js/sidebar-activity.js | 10 ++++- src/inc/locales/de.json | 4 +- src/inc/locales/en.json | 4 +- src/inc/locales/nl.json | 4 +- src/inc/locales/zange.json | 4 +- src/inc/routeinc/f0cklib.mjs | 29 ++++++++++-- views/snippets/footer.html | 4 +- 10 files changed, 167 insertions(+), 13 deletions(-) diff --git a/migrations/f0ckm_schema.sql b/migrations/f0ckm_schema.sql index cc50336..42a3058 100644 --- a/migrations/f0ckm_schema.sql +++ b/migrations/f0ckm_schema.sql @@ -2847,6 +2847,7 @@ CREATE TABLE IF NOT EXISTS public.comment_polls ( id SERIAL PRIMARY KEY, comment_id INTEGER NOT NULL REFERENCES public.comments(id) ON DELETE CASCADE, question TEXT NOT NULL, + is_anonymous BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), expires_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, UNIQUE(comment_id) diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index 13273da..7f03eb6 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -2823,6 +2823,84 @@ body.layout-legacy #comments-container.faded-out { color: #ff4444; } +.poll-anon-badge { + font-size: 0.78em; + color: #555; + display: flex; + align-items: center; + gap: 3px; +} + +.poll-public-badge { + color: #6a9fb5; +} + +.poll-anon-toggle { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 0.8em; + color: #aaa; + cursor: pointer; + user-select: none; + border: 1px solid rgba(255,255,255,0.12); + padding: 3px 10px; + transition: color 0.15s, border-color 0.15s; +} + +.poll-anon-toggle:has(.poll-anon-checkbox:checked) { + color: var(--accent); + border-color: var(--accent); +} + +.poll-anon-checkbox { + display: none; +} + +.poll-option-voters { + display: flex; + flex-wrap: wrap; + gap: 3px; + margin-top: 4px; + align-items: center; +} + +.poll-voter-avatar { + width: 20px; + height: 20px; + border-radius: 50%; + object-fit: cover; + border: 1px solid rgba(255,255,255,0.1); + transition: transform 0.12s, border-color 0.12s; + display: block; +} + +.poll-option-voters a:hover .poll-voter-avatar { + transform: scale(1.2); + border-color: var(--accent); +} + +.sidebar-poll-preview { + display: inline-flex; + align-items: center; + gap: 5px; + margin-top: 4px; + font-size: 0.8em; + color: #888; + text-decoration: none; + font-style: italic; + transition: color 0.12s; +} + +.sidebar-poll-preview i { + color: var(--accent); + opacity: 0.7; +} + +.sidebar-poll-preview:hover { + color: var(--accent); +} + .comments-list { display: flex; diff --git a/public/s/js/comments.js b/public/s/js/comments.js index 5c3a6c6..4fc11c4 100644 --- a/public/s/js/comments.js +++ b/public/s/js/comments.js @@ -2116,19 +2116,29 @@ class CommentSystem { const total = poll.total_votes || 0; const voted = !!poll.user_vote_option_id; const expired = poll.expires_at && new Date(poll.expires_at) < new Date(); + const isAnon = poll.is_anonymous !== false; const canDelete = session.logged_in && (session.is_admin || session.is_moderator || session.user === commentUsername); const optionsHtml = (poll.options || []).map(opt => { const pct = total > 0 ? Math.round((opt.vote_count / total) * 100) : 0; const isVoted = poll.user_vote_option_id === opt.id; - const clickable = session.logged_in && !expired && (!voted); + const clickable = session.logged_in && !expired && !voted; + const voterAvatars = (!isAnon && Array.isArray(opt.voters) && opt.voters.length > 0) + ? `
${opt.voters.map(v => { + const u = (v && typeof v === 'object') ? v : { username: String(v || ''), avatar: null, avatar_file: null }; + const name = String(u.username || ''); + const src = u.avatar_file ? `/a/${u.avatar_file}` : u.avatar ? `/t/${u.avatar}.webp` : '/a/default.png'; + return name ? `${this.escapeHtml(name)}` : ''; + }).join('')}
` + : ''; return `
${this.escapeHtml(opt.text)} ${pct}% ${isVoted ? `` : ''} + ${voterAvatars}
`; }).join(''); @@ -2136,11 +2146,16 @@ class CommentSystem { ? `` : ''; - return `
+ const anonBadge = isAnon + ? `` + : ``; + + return `
${this.escapeHtml(poll.question)}
${optionsHtml}
@@ -2633,6 +2648,10 @@ class CommentSystem {
+
`; @@ -2698,6 +2717,7 @@ class CommentSystem { const i18n = window.f0ckI18n || {}; const total = data.total_votes || 0; pollWidget.querySelector('.poll-total').textContent = `${total} ${total === 1 ? (i18n.poll_vote_single || 'vote') : (i18n.poll_votes || 'votes')}`; + const isAnon = pollWidget.dataset.isAnonymous !== '0'; (data.options || []).forEach(updated => { const el = pollWidget.querySelector(`.poll-option[data-option-id="${updated.id}"]`); if (!el) return; @@ -2710,6 +2730,21 @@ class CommentSystem { el.insertAdjacentHTML('beforeend', ``); } } + // Update voter list for public polls + if (!isAnon && Array.isArray(updated.voters)) { + let votersEl = el.querySelector('.poll-option-voters'); + if (updated.voters.length > 0) { + const html = updated.voters.map(v => { + const u = typeof v === 'object' ? v : { username: v, avatar: null, avatar_file: null }; + const src = u.avatar_file ? `/a/${u.avatar_file}` : u.avatar ? `/t/${u.avatar}.webp` : '/a/default.png'; + return `${u.username}`; + }).join(''); + if (votersEl) votersEl.innerHTML = html; + else el.insertAdjacentHTML('beforeend', `
${html}
`); + } else if (votersEl) { + votersEl.remove(); + } + } }); }).catch(() => { if (window.flashMessage) window.flashMessage('Network error', 2500, 'error'); @@ -3150,8 +3185,9 @@ class CommentSystem { const options = [...pollBuilder.querySelectorAll('.poll-option-input')] .map(i => i.value.trim()) .filter(Boolean); + const isAnonymous = pollBuilder.querySelector('.poll-anon-checkbox')?.checked !== false; if (question && options.length >= 2) { - pollPayload = { question, options }; + pollPayload = { question, options, is_anonymous: isAnonymous }; } } diff --git a/public/s/js/sidebar-activity.js b/public/s/js/sidebar-activity.js index e76b2ae..3a89de0 100644 --- a/public/s/js/sidebar-activity.js +++ b/public/s/js/sidebar-activity.js @@ -410,10 +410,17 @@ return items ? `
${items}
` : ''; }; + const renderSidebarPoll = (poll, commentId, itemId) => { + if (!poll) return ''; + const href = (itemId && commentId) ? `/${itemId}#c${commentId}` : (commentId ? `#sc${commentId}` : '#'); + return ` ${escapeHtml(poll.question)}`; + }; + const renderActivityItem = (c) => { const rawContent = c.content || c.body || ''; const displayContent = renderCommentContent(rawContent, c.id, c.item_id); const attachmentsHtml = renderCommentAttachments(c.files, rawContent); + const pollHtml = renderSidebarPoll(c.poll, c.id, c.item_id); // Build avatar URL — same priority as the rest of the app let avatarSrc = '/a/default.png'; @@ -472,12 +479,13 @@
${timeStr} -
${displayContent}${attachmentsHtml}
+
${displayContent}${attachmentsHtml}${pollHtml}
${itemPreview} `; }; + const checkOverflow = () => { document.querySelectorAll('.sidebar-activity .comment-content-inner').forEach(inner => { const container = inner.parentElement; diff --git a/src/inc/locales/de.json b/src/inc/locales/de.json index 3094d6f..a83f382 100644 --- a/src/inc/locales/de.json +++ b/src/inc/locales/de.json @@ -334,7 +334,9 @@ "poll_votes": "Stimmen", "poll_vote_single": "Stimme", "poll_delete": "Umfrage löschen", - "poll_expired": "Umfrage geschlossen" + "poll_expired": "Umfrage geschlossen", + "poll_anonymous": "Anonym", + "poll_public": "Öffentliche Stimmen" }, "upload_btn": { "select_file": "Datei auswählen", diff --git a/src/inc/locales/en.json b/src/inc/locales/en.json index feeebb2..fa5a11b 100644 --- a/src/inc/locales/en.json +++ b/src/inc/locales/en.json @@ -334,7 +334,9 @@ "poll_votes": "votes", "poll_vote_single": "vote", "poll_delete": "Delete poll", - "poll_expired": "Poll closed" + "poll_expired": "Poll closed", + "poll_anonymous": "Anonymous", + "poll_public": "Public votes" }, "upload_btn": { "select_file": "Select a file", diff --git a/src/inc/locales/nl.json b/src/inc/locales/nl.json index 2329e9f..a3a2363 100644 --- a/src/inc/locales/nl.json +++ b/src/inc/locales/nl.json @@ -334,7 +334,9 @@ "poll_votes": "stemmen", "poll_vote_single": "stem", "poll_delete": "Peiling verwijderen", - "poll_expired": "Peiling gesloten" + "poll_expired": "Peiling gesloten", + "poll_anonymous": "Anoniem", + "poll_public": "Openbare stemmen" }, "upload_btn": { "select_file": "Selecteer een bestand", diff --git a/src/inc/locales/zange.json b/src/inc/locales/zange.json index 2f37efe..c55db23 100644 --- a/src/inc/locales/zange.json +++ b/src/inc/locales/zange.json @@ -332,7 +332,9 @@ "poll_votes": "Stimmen", "poll_vote_single": "Stimme", "poll_delete": "Umfrage löschen", - "poll_expired": "Umfrage geschlossen" + "poll_expired": "Umfrage geschlossen", + "poll_anonymous": "Anonym", + "poll_public": "Öffentliche Stimmen" }, "upload_btn": { "select_file": "Datei auswählen", diff --git a/src/inc/routeinc/f0cklib.mjs b/src/inc/routeinc/f0cklib.mjs index d3b84a5..fefac6a 100644 --- a/src/inc/routeinc/f0cklib.mjs +++ b/src/inc/routeinc/f0cklib.mjs @@ -1020,6 +1020,7 @@ export default { cp.comment_id, cp.question, cp.expires_at, + COALESCE(cp.is_anonymous, true) as is_anonymous, json_agg( json_build_object( 'id', cpo.id, @@ -1037,24 +1038,44 @@ export default { 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 + 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, - options: p.options, + is_anonymous: p.is_anonymous, + options, total_votes: parseInt(p.total_votes) || 0, - user_vote_option_id: null // filled in per-request context if needed + user_vote_option_id: null }); } for (const c of comments) { c.poll = pollMap.get(c.id) || null; } } catch (e) { - // Poll tables might not exist yet + console.error('[POLLS] getComments poll fetch error:', e.message, e.code); for (const c of comments) c.poll = null; } } diff --git a/views/snippets/footer.html b/views/snippets/footer.html index d32c439..aa82aa6 100644 --- a/views/snippets/footer.html +++ b/views/snippets/footer.html @@ -583,7 +583,9 @@ poll_votes: "{{ t('comments.poll_votes') || 'votes' }}", poll_vote_single: "{{ t('comments.poll_vote_single') || 'vote' }}", poll_delete: "{{ t('comments.poll_delete') || 'Delete poll' }}", - poll_expired: "{{ t('comments.poll_expired') || 'Poll closed' }}" + poll_expired: "{{ t('comments.poll_expired') || 'Poll closed' }}", + poll_anonymous: "{{ t('comments.poll_anonymous') || 'Anonymous' }}", + poll_public: "{{ t('comments.poll_public') || 'Public votes' }}" };