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 `
${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 `${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 ``;
+ 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 ``;
}
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,