From 0b2ece9447942f06138c344dce57bfbe68d89532 Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Wed, 19 Aug 2026 21:51:41 +0200 Subject: [PATCH] xddd --- public/s/css/f0ckm.css | 28 +++++++++-- public/s/js/comments.js | 90 ++++++++++++++++++++++++++++++++++-- public/s/js/f0ckm.js | 3 ++ public/s/js/user_comments.js | 6 ++- src/banner_handler.mjs | 12 ++++- src/inc/routeinc/f0cklib.mjs | 4 +- src/inc/routes/comments.mjs | 17 ++++++- src/index.mjs | 3 +- views/snippets/footer.html | 4 ++ 9 files changed, 153 insertions(+), 14 deletions(-) diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index 13c75d7..40ea2ca 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -1043,7 +1043,7 @@ html[theme='95'] .comment, html[theme='95'] .login-form, html[theme='95'] .user-infobox-block { border: 2px inset #dfdfdf !important; - background: #c0c0c0 !important; + background-color: #c0c0c0 !important; color: #000 !important; padding: 10px !important; border-radius: 0 !important; @@ -3100,16 +3100,38 @@ body.layout-legacy .scroll-to-bottom svg { display: flex; gap: 15px; padding: 10px; - background: var(--comment-bg); + margin: 5px; + margin-left: 5px; + background-color: var(--comment-bg); /* Very light seethrough */ border: 1px solid var(--nav-border-color); border-radius: 0; /* No rounded corners */ position: relative; + isolation: isolate; padding-bottom: 25px; /* Room for absolute permalink */ } +.comment > * { + position: relative; + z-index: 1; +} + +.comment::before { + content: ""; + position: absolute; + inset: 0; + background-image: var(--author-banner, none); + background-position: var(--author-banner-position, center top); + background-size: var(--author-banner-size, cover); + background-repeat: var(--author-banner-repeat, repeat); + opacity: var(--author-banner-opacity, 0.25); + z-index: 0; + border-radius: inherit; + pointer-events: none; +} + .comment.deleted { opacity: 0.5; } @@ -9655,7 +9677,7 @@ body.layout-legacy .comment-content img.emoji { .comment-highlighted { box-shadow: inset 3px 0px 0px var(--accent) !important; - background: var(--bg) !important; + background-color: var(--bg) !important; } .anchor-target { diff --git a/public/s/js/comments.js b/public/s/js/comments.js index 41b6aae..f2d3bfa 100644 --- a/public/s/js/comments.js +++ b/public/s/js/comments.js @@ -468,9 +468,15 @@ class CommentSystem { // 2. Check for duplicates (if we just posted it ourselves via optimistic insert). // Even on early return, ensure the button is present if body was truncated. if (document.getElementById('c' + data.id)) { + const el = document.getElementById('c' + data.id); + if (el && data.banner_file && data.banner_file !== 'null') { + el.style.setProperty('--author-banner', `url('/a/${data.banner_file}')`); + el.style.setProperty('--author-banner-position', data.banner_position === 'center' ? 'center top' : (data.banner_position || 'center top')); + el.style.setProperty('--author-banner-size', data.banner_size || 'cover'); + el.style.setProperty('--author-banner-repeat', data.banner_repeat || 'repeat'); + } if (data.body && data.body.endsWith('\u2026')) { console.log('[handleLiveComment] duplicate+truncated, ensuring button for', data.id); - const el = document.getElementById('c' + data.id); const contentEl = el?.querySelector('.comment-content'); if (contentEl && !contentEl.querySelector('.load-full-comment-btn')) { const btnLabel = (window.f0ckI18n?.sidebar_show_full_comment) || 'show full comment'; @@ -507,6 +513,10 @@ class CommentSystem { avatar: data.avatar, avatar_file: data.avatar_file, username_color: data.username_color, + banner_file: data.banner_file || null, + banner_position: data.banner_position || null, + banner_size: data.banner_size || null, + banner_repeat: data.banner_repeat || null, video_time: data.video_time ?? null, is_new: true, is_deleted: false, @@ -593,7 +603,20 @@ class CommentSystem { // Update in-memory data so future reconciles use the full content if (this.lastData) { const cached = this.lastData.find(c => String(c.id) === String(commentId)); - if (cached) cached.content = fullContent; + if (cached) { + cached.content = fullContent; + if (json.comment.banner_file) cached.banner_file = json.comment.banner_file; + if (json.comment.banner_position) cached.banner_position = json.comment.banner_position; + if (json.comment.banner_size) cached.banner_size = json.comment.banner_size; + if (json.comment.banner_repeat) cached.banner_repeat = json.comment.banner_repeat; + } + } + + if (json.comment && json.comment.banner_file) { + el.style.setProperty('--author-banner', `url('/a/${json.comment.banner_file}')`); + el.style.setProperty('--author-banner-position', json.comment.banner_position === 'center' ? 'center top' : (json.comment.banner_position || 'center top')); + el.style.setProperty('--author-banner-size', json.comment.banner_size || 'cover'); + el.style.setProperty('--author-banner-repeat', json.comment.banner_repeat || 'repeat'); } contentEl.dataset.raw = fullContent; @@ -1502,6 +1525,13 @@ class CommentSystem { el.classList.toggle('pinned', !!incoming.is_pinned); const pinIcon = el.querySelector('.pin-icon'); if (pinIcon) pinIcon.style.display = incoming.is_pinned ? 'block' : 'none'; + + if (incoming && incoming.banner_file && incoming.banner_file !== 'null') { + el.style.setProperty('--author-banner', `url('/a/${incoming.banner_file}')`); + el.style.setProperty('--author-banner-position', incoming.banner_position === 'center' ? 'center top' : (incoming.banner_position || 'center top')); + el.style.setProperty('--author-banner-size', incoming.banner_size || 'cover'); + el.style.setProperty('--author-banner-repeat', incoming.banner_repeat || 'repeat'); + } } }); @@ -2170,7 +2200,11 @@ class CommentSystem { } } - return `
${comment.username ? `` : ''}${comment.username ? `` : ''}
${pinnedBadge}${comment.username ? `${this.escapeHtml(comment.display_name || comment.username)}` : 'System'}${contextMarker}${backlinkHtml}
${timeAgo}
${content}
${this.renderCommentAttachments(comment.files, comment.content)}${this.renderCommentPoll(comment.poll, comment.id, comment.username)}
#${comment.id}
${repliesHtml}`; + const bannerStyle = (comment.banner_file && comment.banner_file !== 'null') + ? `style="--author-banner: url('/a/${comment.banner_file}'); --author-banner-position: ${comment.banner_position === 'center' ? 'center top' : (comment.banner_position || 'center top')}; --author-banner-size: ${comment.banner_size || 'cover'}; --author-banner-repeat: ${comment.banner_repeat || 'repeat'};"` + : ''; + + return `
${comment.username ? `` : ''}${comment.username ? `` : ''}
${pinnedBadge}${comment.username ? `${this.escapeHtml(comment.display_name || comment.username)}` : 'System'}${contextMarker}${backlinkHtml}
${timeAgo}
${content}
${this.renderCommentAttachments(comment.files, comment.content)}${this.renderCommentPoll(comment.poll, comment.id, comment.username)}
#${comment.id}
${repliesHtml}`; } timeAgo(date) { @@ -3494,6 +3528,50 @@ class CommentSystem { } } + let resolvedBannerFile = (json.comment?.banner_file && json.comment.banner_file !== 'null') ? json.comment.banner_file : ((session.banner_file && session.banner_file !== 'null') ? session.banner_file : null); + let resolvedBannerPosition = (json.comment?.banner_position && json.comment.banner_position !== 'null') ? json.comment.banner_position : ((session.banner_position && session.banner_position !== 'null') ? session.banner_position : null); + let resolvedBannerSize = (json.comment?.banner_size && json.comment.banner_size !== 'null') ? json.comment.banner_size : ((session.banner_size && session.banner_size !== 'null') ? session.banner_size : null); + let resolvedBannerRepeat = (json.comment?.banner_repeat && json.comment.banner_repeat !== 'null') ? json.comment.banner_repeat : ((session.banner_repeat && session.banner_repeat !== 'null') ? session.banner_repeat : null); + + if (!resolvedBannerFile && this.lastData && currentUsername) { + const existingByMe = this.lastData.find(c => + c.username && c.username.toLowerCase() === currentUsername.toLowerCase() && c.banner_file && c.banner_file !== 'null' + ); + if (existingByMe) { + resolvedBannerFile = existingByMe.banner_file || null; + resolvedBannerPosition = existingByMe.banner_position || null; + resolvedBannerSize = existingByMe.banner_size || null; + resolvedBannerRepeat = existingByMe.banner_repeat || null; + } + } + + if (!resolvedBannerFile && currentUsername) { + const existingCommentEl = document.querySelector(`.comment-author[href="/user/${currentUsername}"], .user-infobox-block[style*="--author-banner"]`); + if (existingCommentEl) { + const commentDiv = existingCommentEl.closest('.comment') || existingCommentEl.closest('.user-infobox-block'); + if (commentDiv) { + const style = commentDiv.getAttribute('style') || ''; + const bannerMatch = style.match(/--author-banner:\s*url\(['"]?\/a\/([^'"]+)['"]?\)/); + if (bannerMatch) { + resolvedBannerFile = bannerMatch[1]; + const posMatch = style.match(/--author-banner-position:\s*([^;]+)/); + if (posMatch) resolvedBannerPosition = posMatch[1].trim(); + const sizeMatch = style.match(/--author-banner-size:\s*([^;]+)/); + if (sizeMatch) resolvedBannerSize = sizeMatch[1].trim(); + const repeatMatch = style.match(/--author-banner-repeat:\s*([^;]+)/); + if (repeatMatch) resolvedBannerRepeat = repeatMatch[1].trim(); + } + } + } + } + + if (resolvedBannerFile && window.f0ckSession) { + window.f0ckSession.banner_file = resolvedBannerFile; + if (resolvedBannerPosition) window.f0ckSession.banner_position = resolvedBannerPosition; + if (resolvedBannerSize) window.f0ckSession.banner_size = resolvedBannerSize; + if (resolvedBannerRepeat) window.f0ckSession.banner_repeat = resolvedBannerRepeat; + } + const newComment = { id: json.comment.id, item_id: this.itemId, @@ -3507,6 +3585,10 @@ class CommentSystem { avatar: resolvedAvatar, avatar_file: resolvedAvatarFile, username_color: session.username_color || null, + banner_file: resolvedBannerFile, + banner_position: resolvedBannerPosition, + banner_size: resolvedBannerSize, + banner_repeat: resolvedBannerRepeat, is_deleted: false, is_pinned: false, video_time: json.comment.video_time ?? null, @@ -3533,7 +3615,7 @@ class CommentSystem { if (!this.lastData) this.lastData = []; const existingInLastData = this.lastData.find(c => c.id === newComment.id); if (existingInLastData) { - existingInLastData.files = files; + Object.assign(existingInLastData, newComment); } else { if (this.sort === 'new') this.lastData.unshift(newComment); else this.lastData.push(newComment); diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js index 2f63d40..a284dae 100644 --- a/public/s/js/f0ckm.js +++ b/public/s/js/f0ckm.js @@ -7436,6 +7436,9 @@ class NotificationSystem { } else if (data.type === 'comments') { window.f0ckDebug(`[SSE] Comment update received:`, data.data); if (data.data.type === 'comment') { + if (window.commentSystem && typeof window.commentSystem.handleLiveComment === 'function') { + window.commentSystem.handleLiveComment(data.data); + } // New comment posted — update xD badge from server-authoritative score if (typeof data.data.xd_score === 'number') { updateXdBadgeFromScore(data.data.item_id, data.data.xd_score); diff --git a/public/s/js/user_comments.js b/public/s/js/user_comments.js index d1fc78f..aba6802 100644 --- a/public/s/js/user_comments.js +++ b/public/s/js/user_comments.js @@ -415,7 +415,11 @@ if (!window.UserCommentSystem) { const fullDate = new Date(c.created_at).toISOString(); const content = this.renderCommentContent(c.content, itemKey); - return `
${this.username}
${timeAgo}
${content}
${this.renderCommentAttachments(c.files, c.content)}${this.renderCommentPoll(c.poll, c.id)}
#${c.id}
`; + const bannerStyle = (c.banner_file && c.banner_file !== 'null') + ? `style="--author-banner: url('/a/${c.banner_file}'); --author-banner-position: ${c.banner_position === 'center' ? 'center top' : (c.banner_position || 'center top')}; --author-banner-size: ${c.banner_size || 'cover'}; --author-banner-repeat: ${c.banner_repeat || 'repeat'};"` + : ''; + + return `
${this.username}
${timeAgo}
${content}
${this.renderCommentAttachments(c.files, c.content)}${this.renderCommentPoll(c.poll, c.id)}
#${c.id}
`; } startLiveTimestamps() { diff --git a/src/banner_handler.mjs b/src/banner_handler.mjs index 9efec35..6b108a4 100644 --- a/src/banner_handler.mjs +++ b/src/banner_handler.mjs @@ -190,15 +190,20 @@ export const handleBannerUpload = async (req, res) => { update user_options set banner_file = ${finalFilename}, banner_position = ${parts.banner_position || 'center'}, - banner_size = ${parts.banner_size || 'cover'} + banner_size = ${parts.banner_size || 'cover'}, + banner_repeat = ${parts.banner_repeat || 'repeat'} where user_id = ${+req.session.id} `; + if (global._invalidateSessionCache && req.cookies && req.cookies.session) { + global._invalidateSessionCache(lib.sha256(req.cookies.session)); + } const payloadStr = JSON.stringify({ user_id: +req.session.id, user: req.session.user, banner_file: finalFilename, banner_position: parts.banner_position || 'center', - banner_size: parts.banner_size || 'cover' + banner_size: parts.banner_size || 'cover', + banner_repeat: parts.banner_repeat || 'repeat' }); await db`select pg_notify('profile_update', ${payloadStr})`; } catch (dbErr) { @@ -273,6 +278,9 @@ export const handleBannerDelete = async (req, res) => { set banner_file = null where user_id = ${+req.session.id} `; + if (global._invalidateSessionCache && req.cookies && req.cookies.session) { + global._invalidateSessionCache(lib.sha256(req.cookies.session)); + } const payloadStr = JSON.stringify({ user_id: +req.session.id, user: req.session.user, diff --git a/src/inc/routeinc/f0cklib.mjs b/src/inc/routeinc/f0cklib.mjs index 299f39d..5f3627c 100644 --- a/src/inc/routeinc/f0cklib.mjs +++ b/src/inc/routeinc/f0cklib.mjs @@ -1313,7 +1313,7 @@ export default { c.id, c.parent_id, c.content, c.created_at, c.vote_score, c.is_deleted, COALESCE(c.is_pinned, false) as is_pinned, c.video_time, - u.user as username, u.id as user_id, uo.avatar, uo.avatar_file, uo.username_color, uo.display_name, + u.user as username, u.id as user_id, uo.avatar, uo.avatar_file, uo.username_color, uo.display_name, uo.banner_file, uo.banner_position, uo.banner_size, uo.banner_repeat, (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 @@ -1432,7 +1432,7 @@ export default { c.id, c.parent_id, c.item_id, c.content, c.created_at, c.vote_score, c.is_deleted, COALESCE(c.is_pinned, false) as is_pinned, c.video_time, - u.user as username, u.id as user_id, uo.avatar, uo.avatar_file, uo.username_color, uo.display_name + u.user as username, u.id as user_id, uo.avatar, uo.avatar_file, uo.username_color, uo.display_name, uo.banner_file, uo.banner_position, uo.banner_size, uo.banner_repeat FROM comments c JOIN "user" u ON c.user_id = u.id LEFT JOIN user_options uo ON uo.user_id = u.id diff --git a/src/inc/routes/comments.mjs b/src/inc/routes/comments.mjs index 8fd3feb..2393c97 100644 --- a/src/inc/routes/comments.mjs +++ b/src/inc/routes/comments.mjs @@ -593,6 +593,9 @@ export default (router, tpl) => { // Large comments would silently drop the notification. The client fetches // the full content via _silentSync; the NOTIFY only needs to trigger the update. const notifyBody = content.length > 500 ? content.substring(0, 500) + '…' : content; + const uo = await db`SELECT banner_file, banner_position, banner_size, banner_repeat FROM user_options WHERE user_id = ${req.session.id}`; + const bannerOpt = uo[0] || {}; + const livePayload = { type: 'comment', id: commentId, @@ -604,6 +607,10 @@ export default (router, tpl) => { user_id: req.session.id, avatar: req.session.avatar, avatar_file: req.session.avatar_file, + banner_file: bannerOpt.banner_file || req.session.banner_file || null, + banner_position: bannerOpt.banner_position || req.session.banner_position || null, + banner_size: bannerOpt.banner_size || req.session.banner_size || null, + banner_repeat: bannerOpt.banner_repeat || req.session.banner_repeat || null, created_at: new Date().toISOString(), username_color: req.session.username_color, display_name: req.session.display_name || null, @@ -631,6 +638,10 @@ export default (router, tpl) => { item_rating_label: ratingLabel, avatar: req.session.avatar, avatar_file: req.session.avatar_file, + banner_file: bannerOpt.banner_file || req.session.banner_file || null, + banner_position: bannerOpt.banner_position || req.session.banner_position || null, + banner_size: bannerOpt.banner_size || req.session.banner_size || null, + banner_repeat: bannerOpt.banner_repeat || req.session.banner_repeat || null, username: req.session.user, username_color: req.session.username_color, display_name: req.session.display_name || null, @@ -654,7 +665,11 @@ export default (router, tpl) => { comment: { ...newComment[0], content, - files: activityFiles + files: activityFiles, + banner_file: bannerOpt.banner_file || req.session.banner_file || null, + banner_position: bannerOpt.banner_position || req.session.banner_position || null, + banner_size: bannerOpt.banner_size || req.session.banner_size || null, + banner_repeat: bannerOpt.banner_repeat || req.session.banner_repeat || null }, xd_score: xdRow?.xd_score ?? null, is_new_subscription diff --git a/src/index.mjs b/src/index.mjs index 26ddd4a..949b2bc 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -501,6 +501,7 @@ process.on('uncaughtException', err => { await runMigration(db`ALTER TABLE user_options ADD COLUMN IF NOT EXISTS banner_file character varying(255) DEFAULT NULL`); await runMigration(db`ALTER TABLE user_options ADD COLUMN IF NOT EXISTS banner_position character varying(50) DEFAULT 'center'`); await runMigration(db`ALTER TABLE user_options ADD COLUMN IF NOT EXISTS banner_size character varying(50) DEFAULT 'cover'`); + await runMigration(db`ALTER TABLE user_options ADD COLUMN IF NOT EXISTS banner_repeat character varying(50) DEFAULT 'repeat'`); await runMigration(db`ALTER TABLE items ADD COLUMN IF NOT EXISTS expires_at bigint DEFAULT NULL`); await runMigration(db`ALTER TABLE items ADD COLUMN IF NOT EXISTS width integer DEFAULT NULL`); await runMigration(db`ALTER TABLE items ADD COLUMN IF NOT EXISTS height integer DEFAULT NULL`); @@ -747,7 +748,7 @@ process.on('uncaughtException', err => { user = [_cachedRow]; } else { user = await db` - select "user".id, "user".login, "user".user, "user".admin, "user".is_moderator, "user".banned, "user".ban_reason, "user".ban_expires, "user".force_password_change, "user_sessions".id as sess_id, "user_sessions".csrf_token, "user_options".mode, "user_options".theme, "user_options".fullscreen, "user_options".excluded_tags, "user_options".avatar, "user_options".avatar_file, "user_options".banner_file, "user_options".banner_position, "user_options".banner_size, "user_options".show_motd, "user_options".strict_mode, "user_options".show_background, "user_options".use_new_layout, "user_options".username_color, "user_options".font, "user_options".disable_autoplay, "user_options".disable_swiping, "user_options".favorites_private, "user_options".hide_fav_badge, "user_options".default_upload_visibility, "user_options".description, "user_options".display_name, COALESCE("user_options".min_xd_score, 0) as min_xd_score, "user_options".ruffle_volume, "user_options".ruffle_background, "user_options".quote_emojis, "user_options".embed_youtube_in_comments, "user_options".hide_koepfe, "user_options".language, "user_options".use_alternative_infobox, "user_options".use_alternative_steuerung, "user_options".receive_system_notifications, "user_options".receive_user_notifications, "user_options".do_not_disturb, "user_options".comment_display_mode, "user_options".force_comment_display_mode + select "user".id, "user".login, "user".user, "user".admin, "user".is_moderator, "user".banned, "user".ban_reason, "user".ban_expires, "user".force_password_change, "user_sessions".id as sess_id, "user_sessions".csrf_token, "user_options".mode, "user_options".theme, "user_options".fullscreen, "user_options".excluded_tags, "user_options".avatar, "user_options".avatar_file, "user_options".banner_file, "user_options".banner_position, "user_options".banner_size, "user_options".banner_repeat, "user_options".show_motd, "user_options".strict_mode, "user_options".show_background, "user_options".use_new_layout, "user_options".username_color, "user_options".font, "user_options".disable_autoplay, "user_options".disable_swiping, "user_options".favorites_private, "user_options".hide_fav_badge, "user_options".default_upload_visibility, "user_options".description, "user_options".display_name, COALESCE("user_options".min_xd_score, 0) as min_xd_score, "user_options".ruffle_volume, "user_options".ruffle_background, "user_options".quote_emojis, "user_options".embed_youtube_in_comments, "user_options".hide_koepfe, "user_options".language, "user_options".use_alternative_infobox, "user_options".use_alternative_steuerung, "user_options".receive_system_notifications, "user_options".receive_user_notifications, "user_options".do_not_disturb, "user_options".comment_display_mode, "user_options".force_comment_display_mode from "user_sessions" left join "user" on "user".id = "user_sessions".user_id left join "user_options" on "user_options".user_id = "user_sessions".user_id diff --git a/views/snippets/footer.html b/views/snippets/footer.html index bbf192b..459518b 100644 --- a/views/snippets/footer.html +++ b/views/snippets/footer.html @@ -391,6 +391,10 @@ embed_youtube_in_comments: @if(session && session.embed_youtube_in_comments !== false) true @else false @endif, avatar: @if(session && session.avatar) {{ session.avatar }} @else null @endif, avatar_file: @if(session && session.avatar_file) "{{ session.avatar_file }}" @else null @endif, + banner_file: @if(session && session.banner_file) "{{ session.banner_file }}" @else null @endif, + banner_position: @if(session && session.banner_position) "{{ session.banner_position }}" @else null @endif, + banner_size: @if(session && session.banner_size) "{{ session.banner_size }}" @else null @endif, + banner_repeat: @if(session && session.banner_repeat) "{{ session.banner_repeat }}" @else null @endif, receive_system_notifications: @if(session)@if(session.receive_system_notifications !== false) true @else false @endif@else true @endif, receive_user_notifications: @if(session)@if(session.receive_user_notifications !== false) true @else false @endif@else true @endif, do_not_disturb: @if(session && session.do_not_disturb) true @else false @endif,