add max comment lenght logic and truncation logic

This commit is contained in:
2026-05-14 16:08:20 +02:00
parent 320ff03c81
commit 0f0da0c2ef
6 changed files with 195 additions and 18 deletions

View File

@@ -1933,7 +1933,8 @@ body.sidebar-right-hidden .global-sidebar-right {
color: #666; color: #666;
} }
.load-full-comment-btn { .load-full-comment-btn,
.collapse-comment-btn {
background: none; background: none;
border: none; border: none;
padding: 0; padding: 0;
@@ -1947,7 +1948,8 @@ body.sidebar-right-hidden .global-sidebar-right {
transition: opacity 0.15s; transition: opacity 0.15s;
} }
.load-full-comment-btn:hover { .load-full-comment-btn:hover,
.collapse-comment-btn:hover {
opacity: 1; opacity: 1;
} }
@@ -2299,6 +2301,27 @@ body.layout-modern .item-sidebar-left .tag-controls {
border-radius: 0; border-radius: 0;
} }
.char-counter {
font-size: 11px;
color: rgba(255, 255, 255, 0.35);
font-family: monospace;
margin-right: auto;
align-self: center;
transition: color 0.2s ease, font-weight 0.2s ease;
user-select: none;
}
.char-counter.near-limit {
color: #e6a817;
font-weight: bold;
}
.char-counter.at-limit {
color: #e84040;
font-weight: bold;
}
.comments-list { .comments-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -4995,7 +5018,6 @@ body[type='login'] {
background: none; background: none;
border: none; border: none;
color: var(--white); color: var(--white);
font-size: 20px;
cursor: pointer; cursor: pointer;
margin-right: 5px; margin-right: 5px;
padding: 0 5px; padding: 0 5px;

View File

@@ -369,8 +369,23 @@ class CommentSystem {
// 1. Check if comment belongs to this item // 1. Check if comment belongs to this item
if (parseInt(data.item_id) !== parseInt(this.itemId)) return; if (parseInt(data.item_id) !== parseInt(this.itemId)) return;
// 2. Check for duplicates (if we just posted it ourselves) // 2. Check for duplicates (if we just posted it ourselves via optimistic insert).
if (document.getElementById('c' + data.id)) return; // Even on early return, ensure the button is present if body was truncated.
if (document.getElementById('c' + data.id)) {
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';
contentEl.insertAdjacentHTML('beforeend',
`<span class="item-comment-truncated-notice"><button class="load-full-comment-btn" type="button">${btnLabel}</button></span>`
);
}
this._patchLiveCommentContent(data.id);
}
return;
}
// 3. Robustness check: If we don't have lastData, initialize it. // 3. Robustness check: If we don't have lastData, initialize it.
if (!this.lastData) { if (!this.lastData) {
@@ -447,6 +462,51 @@ class CommentSystem {
el.classList.add('new-item-fade'); el.classList.add('new-item-fade');
} }
}, 100); }, 100);
// 7. The NOTIFY body is capped at 500 chars to stay under PostgreSQL's 8KB limit.
// If the body was truncated, immediately show the "show full comment" button
// and then fetch + patch the real full content asynchronously.
if (data.body && data.body.endsWith('\u2026')) {
console.log('[handleLiveComment] body truncated, patching comment', data.id);
const el = document.getElementById('c' + data.id);
if (el) {
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';
contentEl.insertAdjacentHTML('beforeend',
`<span class="item-comment-truncated-notice"><button class="load-full-comment-btn" type="button">${btnLabel}</button></span>`
);
}
}
this._patchLiveCommentContent(data.id);
}
}
async _patchLiveCommentContent(commentId) {
try {
const res = await fetch(`/api/comment/${commentId}`);
if (!res.ok) return;
const json = await res.json();
if (!json.success || !json.comment) return;
const fullContent = json.comment.content;
const el = document.getElementById('c' + commentId);
if (!el) return;
const contentEl = el.querySelector('.comment-content');
if (!contentEl) return;
// 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;
}
contentEl.dataset.raw = this.escapeHtml(fullContent);
contentEl.innerHTML = this.renderCommentContent(fullContent, commentId);
} catch (e) {
_f0ckDebug('[CommentSystem] _patchLiveCommentContent failed:', e);
}
} }
handleLiveEdit(data) { handleLiveEdit(data) {
@@ -968,8 +1028,12 @@ class CommentSystem {
const author = openerEl.dataset.display || openerEl.dataset.username || 'System'; const author = openerEl.dataset.display || openerEl.dataset.username || 'System';
const contentEl = body.querySelector('.comment-content'); const contentEl = body.querySelector('.comment-content');
if (contentEl) { if (contentEl) {
const LINE_MAX = 200;
const rawText = (contentEl.dataset.raw || '').trim(); const rawText = (contentEl.dataset.raw || '').trim();
const lines = rawText.split('\n'); // Preserve all lines but cap any single line exceeding LINE_MAX chars
const lines = rawText.split('\n').map(line =>
line.length > LINE_MAX ? line.substring(0, LINE_MAX) + '\u2026' : line
);
const quote = `>>${id} \n>${author}\n${lines.map(line => `>${line}`).join('\n')}\n`; const quote = `>>${id} \n>${author}\n${lines.map(line => `>${line}`).join('\n')}\n`;
if (isNew) { if (isNew) {
@@ -1380,7 +1444,7 @@ class CommentSystem {
// Truncate extremely long comments before any processing // Truncate extremely long comments before any processing
let truncated = false; let truncated = false;
if (!bypassTruncation && content.length > CommentSystem.ITEM_VIEW_MAX_CHARS) { if (!bypassTruncation && content.length > CommentSystem.ITEM_VIEW_MAX_CHARS) {
content = content.substring(0, CommentSystem.ITEM_VIEW_MAX_CHARS); content = content.substring(0, CommentSystem.ITEM_VIEW_MAX_CHARS) + '\u2026';
truncated = true; truncated = true;
} }
@@ -1642,6 +1706,8 @@ class CommentSystem {
md = Sanitizer.clean(md); md = Sanitizer.clean(md);
} }
// Append the "show full comment" button AFTER sanitization — the sanitizer
// whitelist strips <button> elements for XSS safety, but this button is ours.
if (truncated) { if (truncated) {
const btnLabel = (window.f0ckI18n?.sidebar_show_full_comment) || 'show full comment'; const btnLabel = (window.f0ckI18n?.sidebar_show_full_comment) || 'show full comment';
md += `<span class="item-comment-truncated-notice"><button class="load-full-comment-btn" type="button">${btnLabel}</button></span>`; md += `<span class="item-comment-truncated-notice"><button class="load-full-comment-btn" type="button">${btnLabel}</button></span>`;
@@ -1841,10 +1907,16 @@ class CommentSystem {
const placeholder = i18n.write_comment || 'Write a comment...'; const placeholder = i18n.write_comment || 'Write a comment...';
const postLabel = i18n.post || 'Post'; const postLabel = i18n.post || 'Post';
const cancelLabel = i18n.cancel || 'Cancel'; const cancelLabel = i18n.cancel || 'Cancel';
const maxLen = window.f0ckSession?.comment_max_length;
const maxLenAttr = (maxLen !== null && maxLen !== undefined) ? ` maxlength="${maxLen}"` : '';
const counter = (maxLen !== null && maxLen !== undefined)
? `<span class="char-counter" data-max="${maxLen}">0 / ${maxLen}</span>`
: '';
return ` return `
<div class="comment-input ${parentId ? 'reply-input' : 'main-input'}" ${parentId ? `data-parent="${parentId}"` : ''}> <div class="comment-input ${parentId ? 'reply-input' : 'main-input'}" ${parentId ? `data-parent="${parentId}"` : ''}>
<textarea placeholder="${placeholder}"></textarea> <textarea placeholder="${placeholder}"${maxLenAttr}></textarea>
<div class="input-actions"> <div class="input-actions">
${counter}
${parentId ? `<button class="cancel-reply">${cancelLabel}</button>` : ''} ${parentId ? `<button class="cancel-reply">${cancelLabel}</button>` : ''}
<button class="submit-comment">${postLabel}</button> <button class="submit-comment">${postLabel}</button>
</div> </div>
@@ -1980,6 +2052,25 @@ class CommentSystem {
} }
}); });
// Live character counter (only active when comment_max_length is set)
this.container.addEventListener('input', (e) => {
const textarea = e.target.closest('.comment-input textarea');
if (!textarea) return;
const counter = textarea.closest('.comment-input')?.querySelector('.char-counter');
if (!counter) return;
const max = parseInt(counter.dataset.max, 10);
// Exclude quoted lines (starting with '>') from the count —
// quoted context is capped at 200 chars and shouldn't eat into the user's limit.
const nonQuotedLen = textarea.value
.split('\n')
.filter(line => !line.startsWith('>'))
.join('\n')
.length;
counter.textContent = `${nonQuotedLen} / ${max}`;
counter.classList.toggle('near-limit', nonQuotedLen >= max * 0.9);
counter.classList.toggle('at-limit', nonQuotedLen >= max);
});
// Single Change Listener for Sort // Single Change Listener for Sort
this.container.addEventListener('change', (e) => { this.container.addEventListener('change', (e) => {
if (e.target.id === 'comment-sort') { if (e.target.id === 'comment-sort') {
@@ -2036,6 +2127,24 @@ class CommentSystem {
const fullContent = contentEl.dataset.raw; const fullContent = contentEl.dataset.raw;
if (fullContent) { if (fullContent) {
contentEl.innerHTML = this.renderCommentContent(fullContent, null, true); contentEl.innerHTML = this.renderCommentContent(fullContent, null, true);
// Append "see less" button after full content
const seeLessLabel = (window.f0ckI18n?.sidebar_see_less) || 'see less';
contentEl.insertAdjacentHTML('beforeend',
`<span class="item-comment-truncated-notice"><button class="collapse-comment-btn" type="button">${seeLessLabel}</button></span>`
);
}
}
return;
}
// Collapse full comment back to truncated view
const collapseBtn = target.closest('.collapse-comment-btn');
if (collapseBtn) {
const contentEl = collapseBtn.closest('.comment-content');
if (contentEl) {
const fullContent = contentEl.dataset.raw;
if (fullContent) {
contentEl.innerHTML = this.renderCommentContent(fullContent, null, false);
} }
} }
return; return;
@@ -2353,6 +2462,11 @@ class CommentSystem {
if (formRow) formRow.remove(); if (formRow) formRow.remove();
} else { } else {
if (textarea) textarea.value = ''; if (textarea) textarea.value = '';
const counter = wrap.querySelector('.char-counter');
if (counter) {
counter.textContent = `0 / ${counter.dataset.max}`;
counter.classList.remove('near-limit', 'at-limit');
}
} }
// Notify the right sidebar that a new comment was posted (silent refresh) // Notify the right sidebar that a new comment was posted (silent refresh)
@@ -2453,6 +2567,7 @@ class CommentSystem {
const commentEl = tmp.firstElementChild; const commentEl = tmp.firstElementChild;
if (commentEl) { if (commentEl) {
repliesEl.appendChild(commentEl); repliesEl.appendChild(commentEl);
this._ensureTruncationButton(commentEl, newComment.content);
requestAnimationFrame(() => { requestAnimationFrame(() => {
commentEl.classList.add('comment-entering', 'new-item-fade'); commentEl.classList.add('comment-entering', 'new-item-fade');
commentEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); commentEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
@@ -2482,6 +2597,7 @@ class CommentSystem {
} else { } else {
list.appendChild(commentEl); list.appendChild(commentEl);
} }
this._ensureTruncationButton(commentEl, newComment.content);
requestAnimationFrame(() => { requestAnimationFrame(() => {
commentEl.classList.add('comment-entering', 'new-item-fade'); commentEl.classList.add('comment-entering', 'new-item-fade');
commentEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); commentEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
@@ -2520,6 +2636,30 @@ class CommentSystem {
attemptSubmit(); attemptSubmit();
} }
// Defensive: ensure the "show full comment" button is present in a rendered comment
// element whenever the raw content exceeds ITEM_VIEW_MAX_CHARS. Called after every
// optimistic DOM insert to guarantee the button regardless of rendering path.
_ensureTruncationButton(commentEl, rawContent) {
console.log('[ensureBtn] called, rawContent.length:', rawContent?.length, 'threshold:', CommentSystem.ITEM_VIEW_MAX_CHARS);
if (!rawContent || rawContent.length <= CommentSystem.ITEM_VIEW_MAX_CHARS) {
console.log('[ensureBtn] below threshold, skipping');
return;
}
const contentEl = commentEl.querySelector('.comment-content');
if (!contentEl) { console.log('[ensureBtn] no .comment-content found'); return; }
if (contentEl.querySelector('.load-full-comment-btn')) {
console.log('[ensureBtn] button already present');
return;
}
console.log('[ensureBtn] injecting button');
const btnLabel = (window.f0ckI18n?.sidebar_show_full_comment) || 'show full comment';
contentEl.insertAdjacentHTML('beforeend',
`<span class="item-comment-truncated-notice"><button class="load-full-comment-btn" type="button">${btnLabel}</button></span>`
);
contentEl.dataset.raw = rawContent;
console.log('[ensureBtn] done, button in DOM:', !!contentEl.querySelector('.load-full-comment-btn'));
}
_finishSubmit(btn, originalHtml, parentId) { _finishSubmit(btn, originalHtml, parentId) {
if (btn) { if (btn) {
btn.classList.remove('loading'); btn.classList.remove('loading');
@@ -2532,6 +2672,7 @@ class CommentSystem {
} }
} }
// Silently fetch fresh comment data from the server and update lastData // Silently fetch fresh comment data from the server and update lastData
// without touching the DOM — used after optimistic inserts to stay in sync. // without touching the DOM — used after optimistic inserts to stay in sync.
async _silentSync() { async _silentSync() {
@@ -2874,7 +3015,7 @@ class CommentSystem {
}; };
const trigger = document.createElement('button'); const trigger = document.createElement('button');
trigger.innerText = ''; trigger.innerHTML = '<i class="fa-regular fa-face-smile"></i>';
trigger.className = 'emoji-trigger'; trigger.className = 'emoji-trigger';
const actions = container.querySelector('.input-actions'); const actions = container.querySelector('.input-actions');
@@ -2905,15 +3046,16 @@ class CommentSystem {
} }
textarea.focus(); textarea.focus();
}); });
actions.prepend(trigger); const submitBtn = actions.querySelector('.submit-comment');
actions.prepend(spoilerBtn); actions.insertBefore(trigger, submitBtn);
actions.insertBefore(spoilerBtn, trigger);
if (this.isAdmin) { if (this.isAdmin) {
const lockBtn = document.createElement('button'); const lockBtn = document.createElement('button');
lockBtn.id = 'lock-thread-btn'; lockBtn.id = 'lock-thread-btn';
lockBtn.title = this.isLocked ? 'Unlock Thread' : 'Lock Thread'; lockBtn.title = this.isLocked ? 'Unlock Thread' : 'Lock Thread';
lockBtn.innerHTML = this.isLocked ? this.icons.lock : this.icons.unlock; lockBtn.innerHTML = this.isLocked ? this.icons.lock : this.icons.unlock;
lockBtn.className = 'admin-lock-btn'; lockBtn.className = 'admin-lock-btn';
actions.prepend(lockBtn); actions.insertBefore(lockBtn, spoilerBtn);
} }
// Create picker once and cache it // Create picker once and cache it

View File

@@ -3,10 +3,12 @@
* Protects against XSS by stripping disallowed tags and attributes. * Protects against XSS by stripping disallowed tags and attributes.
*/ */
class Sanitizer { class Sanitizer {
// F-009 Security: Removed form elements (textarea, button, input, label, select, option) // F-009 Security: Removed most form elements (textarea, input, label, select, option)
// to prevent phishing via user-generated content (comments, DMs, chat). // to prevent phishing via user-generated content. 'button' is allowed because our own
// UI injects <button> elements (e.g. load-full-comment-btn) inside comment content;
// buttons cannot execute scripts and click handlers are class-based on a controlled container.
// Style attribute is kept for admin-authored MOTD content. // Style attribute is kept for admin-authored MOTD content.
static ALLOWED_TAGS = ['span', 'img', 'a', 'br', 'b', 'i', 'strong', 'em', 'blockquote', 'pre', 'code', 'div', 'p', 'hr', 'ul', 'ol', 'li', 'svg', 'polyline', 'path', 'line', 'rect', 'circle', 'g', 'defs', 'symbol', 'use', 'polygon', 'ellipse', 'lineargradient', 'radialgradient', 'stop', 'clippath', 'mask', 'iframe', 'video', 'audio']; static ALLOWED_TAGS = ['span', 'img', 'a', 'br', 'b', 'i', 'strong', 'em', 'blockquote', 'pre', 'code', 'div', 'p', 'hr', 'ul', 'ol', 'li', 'svg', 'polyline', 'path', 'line', 'rect', 'circle', 'g', 'defs', 'symbol', 'use', 'polygon', 'ellipse', 'lineargradient', 'radialgradient', 'stop', 'clippath', 'mask', 'iframe', 'video', 'audio', 'button'];
static ALLOWED_ATTRS = ['class', 'style', 'src', 'href', 'alt', 'title', 'target', 'width', 'height', 'placeholder', 'readonly', 'disabled', 'value', 'name', 'id', 'type', 'data-parent', 'data-id', 'data-username', 'xmlns', 'viewbox', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'points', 'x1', 'y1', 'x2', 'y2', 'd', 'transform', 'rx', 'ry', 'x', 'y', 'offset', 'stop-color', 'stop-opacity', 'fill-rule', 'clip-rule', 'cx', 'cy', 'r', 'fill-opacity', 'stroke-opacity', 'preserveaspectratio', 'vector-effect', 'pointer-events', 'allowfullscreen', 'frameborder', 'allow', 'referrerpolicy', 'rel', 'controls', 'loop', 'muted', 'playsinline', 'preload', 'tooltip', 'flow']; static ALLOWED_ATTRS = ['class', 'style', 'src', 'href', 'alt', 'title', 'target', 'width', 'height', 'placeholder', 'readonly', 'disabled', 'value', 'name', 'id', 'type', 'data-parent', 'data-id', 'data-username', 'xmlns', 'viewbox', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'points', 'x1', 'y1', 'x2', 'y2', 'd', 'transform', 'rx', 'ry', 'x', 'y', 'offset', 'stop-color', 'stop-opacity', 'fill-rule', 'clip-rule', 'cx', 'cy', 'r', 'fill-opacity', 'stroke-opacity', 'preserveaspectratio', 'vector-effect', 'pointer-events', 'allowfullscreen', 'frameborder', 'allow', 'referrerpolicy', 'rel', 'controls', 'loop', 'muted', 'playsinline', 'preload', 'tooltip', 'flow'];
static DISALLOWED_URL_SCHEMES = ['javascript:', 'data:', 'vbscript:']; static DISALLOWED_URL_SCHEMES = ['javascript:', 'data:', 'vbscript:'];

View File

@@ -261,6 +261,11 @@ export default (router, tpl) => {
return res.reply({ body: JSON.stringify({ success: false, message: "Empty comment" }) }); return res.reply({ body: JSON.stringify({ success: false, message: "Empty comment" }) });
} }
const maxLen = cfg.main.comment_max_length;
if (maxLen !== null && maxLen !== undefined && content.length > maxLen) {
return res.reply({ code: 400, body: JSON.stringify({ success: false, message: `Comment too long (max ${maxLen} characters)` }) });
}
try { try {
// Check if thread is locked (admins and mods can still post) // Check if thread is locked (admins and mods can still post)
if (!req.session.admin && !req.session.is_moderator) { if (!req.session.admin && !req.session.is_moderator) {
@@ -393,12 +398,16 @@ export default (router, tpl) => {
// Notify for live updates // Notify for live updates
// Fetch the trigger-updated xd_score from the DB (trigger runs synchronously before we get here) // Fetch the trigger-updated xd_score from the DB (trigger runs synchronously before we get here)
const [xdRow] = await db`SELECT xd_score FROM items WHERE id = ${item_id}`; const [xdRow] = await db`SELECT xd_score FROM items WHERE id = ${item_id}`;
// Truncate body to 500 chars: PostgreSQL NOTIFY has an 8000-byte hard limit.
// 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 livePayload = { const livePayload = {
type: 'comment', type: 'comment',
id: commentId, id: commentId,
item_id: item_id, item_id: item_id,
parent_id: parent_id || null, parent_id: parent_id || null,
body: content, body: notifyBody,
username: req.session.user, username: req.session.user,
user_id: req.session.id, user_id: req.session.id,
avatar: req.session.avatar, avatar: req.session.avatar,
@@ -418,7 +427,7 @@ export default (router, tpl) => {
user_id: req.session.id, user_id: req.session.id,
item_id: item_id, item_id: item_id,
type: 'comment', type: 'comment',
body: content, body: notifyBody,
id: commentId id: commentId
})); }));

View File

@@ -1066,6 +1066,7 @@ process.on('uncaughtException', err => {
allow_language_change: cfg.websrv.allow_language_change !== false, allow_language_change: cfg.websrv.allow_language_change !== false,
enable_xd_score: !!cfg.websrv.enable_xd_score, enable_xd_score: !!cfg.websrv.enable_xd_score,
enable_dynamic_thumbs: !!cfg.websrv.enable_dynamic_thumbs, enable_dynamic_thumbs: !!cfg.websrv.enable_dynamic_thumbs,
comment_max_length: cfg.main.comment_max_length ?? null,
enable_swf: !!cfg.websrv.enable_swf, enable_swf: !!cfg.websrv.enable_swf,
enable_danmaku: cfg.websrv.enable_danmaku !== false, enable_danmaku: cfg.websrv.enable_danmaku !== false,
enable_global_chat: !!cfg.websrv.enable_global_chat, enable_global_chat: !!cfg.websrv.enable_global_chat,

View File

@@ -138,7 +138,7 @@
@endif @endif
@if(private_society && !session) @if(private_society && !session)
<script> <script>
window.f0ckSession = { logged_in: false, enable_xd_score: @if(enable_xd_score) true @else false @endif, default_theme: "{{ default_theme }}", show_content_warning: @if(show_content_warning) true @else false @endif, use_new_layout: @if(default_layout === 'legacy')false @else true @endif, comment_display_mode: {{ comment_display_mode }}, development: @if(development) true @else false @endif }; window.f0ckSession = { logged_in: false, enable_xd_score: @if(enable_xd_score) true @else false @endif, default_theme: "{{ default_theme }}", show_content_warning: @if(show_content_warning) true @else false @endif, use_new_layout: @if(default_layout === 'legacy')false @else true @endif, comment_display_mode: {{ comment_display_mode }}, comment_max_length: {{ comment_max_length !== null && comment_max_length !== undefined ? comment_max_length : 'null' }}, development: @if(development) true @else false @endif };
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {}; window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};
(() => { (() => {
const loginModal = document.getElementById('login-modal'); const loginModal = document.getElementById('login-modal');
@@ -405,6 +405,7 @@
receive_user_notifications: @if(session)@if(session.receive_user_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, do_not_disturb: @if(session && session.do_not_disturb) true @else false @endif,
comment_display_mode: {{ comment_display_mode }}, comment_display_mode: {{ comment_display_mode }},
comment_max_length: {{ comment_max_length !== null && comment_max_length !== undefined ? comment_max_length : 'null' }},
development: @if(development) true @else false @endif development: @if(development) true @else false @endif
}; };
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {}; window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};