#12 first commit

This commit is contained in:
2026-05-16 20:11:51 +02:00
parent fbd47636d1
commit 552c239677
16 changed files with 962 additions and 12 deletions

View File

@@ -2354,6 +2354,144 @@ body.layout-modern .item-sidebar-left .tag-controls {
font-weight: bold;
}
/* ─── Comment File Upload ─────────────────────────────────────────────────── */
.comment-attach-btn {
background: none;
border: 1px solid var(--nav-border-color);
color: #888;
cursor: pointer;
padding: 4px 8px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.15s, border-color 0.15s;
font-size: 14px;
}
.comment-attach-btn:hover {
color: var(--accent);
border-color: var(--accent);
}
.comment-file-preview {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 0;
}
.comment-file-preview:empty {
display: none;
}
.cf-preview-item {
position: relative;
display: flex;
align-items: center;
gap: 6px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 4px 6px;
max-width: 220px;
}
.cf-preview-item img,
.cf-preview-item video {
width: 48px;
height: 48px;
object-fit: cover;
flex-shrink: 0;
}
.cf-preview-item i.fa-music {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.05);
color: var(--accent);
font-size: 20px;
flex-shrink: 0;
}
.cf-filename {
font-size: 0.75em;
color: #aaa;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 110px;
}
.cf-remove-btn {
background: none;
border: none;
color: #888;
cursor: pointer;
padding: 2px;
font-size: 12px;
flex-shrink: 0;
transition: color 0.15s;
}
.cf-remove-btn:hover {
color: #ff4444;
}
.cf-preview-item.cf-uploading {
opacity: 0.6;
}
.cf-preview-item.cf-uploading .fa-spinner {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: var(--accent);
}
/* ─── Comment Attachments (rendered below comment content) ────────────────── */
.comment-attachments {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.cf-attachment {
display: inline-block;
max-width: 100%;
}
.cf-attachment.cf-image img {
max-width: 300px;
max-height: 220px;
object-fit: contain;
border: 1px solid rgba(255, 255, 255, 0.08);
cursor: pointer;
transition: opacity 0.15s;
}
.cf-attachment.cf-image img:hover {
opacity: 0.85;
}
.cf-attachment.cf-video video {
max-width: 400px;
max-height: 280px;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.cf-attachment.cf-audio audio {
width: 100%;
max-width: 350px;
}
.comments-list {
display: flex;
@@ -8092,7 +8230,7 @@ span.badge.badge-current {
.comment-content img:not(.emoji) {
display: block;
cursor: pointer;
}
/* Smaller emojis in the narrow left sidebar */

View File

@@ -1858,6 +1858,7 @@ class CommentSystem {
<a href="#c${comment.id}" class="comment-time timeago" title="${fullDate}" data-id="${comment.id}" data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}">${timeAgo}</a>
</div>
<div class="comment-content" data-raw="${this.escapeHtml(comment.content)}">${content}</div>
${this.renderCommentAttachments(comment.files)}
<div class="comment-footer">
<div class="comment-footer-right">
<div class="comment-actions">
@@ -1901,22 +1902,46 @@ class CommentSystem {
return div.innerHTML;
}
renderCommentAttachments(files) {
if (!files || files.length === 0) return '';
const items = files.map(f => {
const url = `/c/${f.dest}`;
if (f.mime.startsWith('image/')) {
return `<a href="${url}" target="_blank" class="cf-attachment cf-image"><img src="${url}" alt="${this.escapeHtml(f.original_filename || 'image')}" loading="lazy"></a>`;
} else if (f.mime.startsWith('video/')) {
return `<div class="cf-attachment cf-video"><video src="${url}" controls preload="metadata"></video></div>`;
} else if (f.mime.startsWith('audio/')) {
return `<div class="cf-attachment cf-audio"><audio src="${url}" controls preload="metadata"></audio></div>`;
}
return '';
}).join('');
return items ? `<div class="comment-attachments">${items}</div>` : '';
}
renderInput(parentId = null) {
const i18n = window.f0ckI18n || {};
const session = window.f0ckSession || {};
const placeholder = i18n.write_comment || 'Write a comment...';
const postLabel = i18n.post || 'Post';
const cancelLabel = i18n.cancel || 'Cancel';
const maxLen = window.f0ckSession?.comment_max_length;
const attachLabel = i18n.attach_file || 'Attach file';
const maxLen = session.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>`
: '';
const fileUploadEnabled = session.logged_in && session.allow_fileupload_comments;
const multiFile = session.fileupload_comments_multifile;
const attachBtn = fileUploadEnabled
? `<button class="comment-attach-btn" title="${attachLabel}" type="button"><i class="fa-solid fa-paperclip"></i></button><input type="file" class="comment-file-input" accept="image/*,video/*,audio/*" ${multiFile ? 'multiple' : ''} style="display:none;">`
: '';
return `
<div class="comment-input ${parentId ? 'reply-input' : 'main-input'}" ${parentId ? `data-parent="${parentId}"` : ''}>
<textarea placeholder="${placeholder}"${maxLenAttr}></textarea>
<div class="comment-file-preview"></div>
<div class="input-actions">
${counter}
${attachBtn}
${parentId ? `<button class="cancel-reply" title="${cancelLabel}"><i class="fa-solid fa-xmark"></i></button>` : ''}
<button class="submit-comment">${postLabel}</button>
</div>
@@ -2080,6 +2105,113 @@ class CommentSystem {
});
// Single Click Listener for Everything
this.container.addEventListener('change', async (e) => {
if (!e.target.matches('.comment-file-input')) return;
const fileInput = e.target;
const wrap = fileInput.closest('.comment-input');
if (!wrap) return;
const textarea = wrap.querySelector('textarea');
if (!textarea) return;
const previewArea = wrap.querySelector('.comment-file-preview');
const session = window.f0ckSession || {};
const maxSize = session.fileupload_comments_size || (10 * 1024 * 1024);
const i18n = window.f0ckI18n || {};
const removeLabel = i18n.remove_file || 'Remove file';
for (const file of fileInput.files) {
if (file.size > maxSize) {
alert((i18n.file_too_large || 'File too large') + `: ${file.name}`);
continue;
}
const fd = new FormData();
fd.append('file', file);
const csrf = session.csrf_token || '';
const uploadingText = i18n.uploading_file || 'Uploading...';
// Insert placeholder at cursor position
const cursorPos = textarea.selectionStart;
const before = textarea.value.substring(0, cursorPos);
const after = textarea.value.substring(textarea.selectionEnd);
const placeholder = `[${uploadingText} ${file.name}]`;
const sep = before.length > 0 && !/\s$/.test(before) ? ' ' : '';
textarea.value = before + sep + placeholder + after;
// Show uploading preview
let previewItem = null;
if (previewArea) {
previewItem = document.createElement('div');
previewItem.className = 'cf-preview-item cf-uploading';
const spinner = document.createElement('i');
spinner.className = 'fa-solid fa-spinner fa-spin';
previewItem.appendChild(spinner);
const nameEl = document.createElement('span');
nameEl.className = 'cf-filename';
nameEl.textContent = file.name;
previewItem.appendChild(nameEl);
previewArea.appendChild(previewItem);
}
try {
const res = await fetch('/api/v2/comments/upload', {
method: 'POST',
headers: { 'X-CSRF-Token': csrf },
body: fd
});
const json = await res.json();
if (json.success && json.files && json.files.length > 0) {
const fileData = json.files[0];
const url = `/c/${fileData.dest}`;
textarea.value = textarea.value.replace(placeholder, url);
// Update preview with actual thumbnail
if (previewItem) {
previewItem.classList.remove('cf-uploading');
previewItem.dataset.url = url;
previewItem.dataset.fileId = fileData.id;
previewItem.innerHTML = '';
if (fileData.mime.startsWith('image/')) {
const img = document.createElement('img');
img.src = url;
img.loading = 'lazy';
previewItem.appendChild(img);
} else if (fileData.mime.startsWith('video/')) {
const vid = document.createElement('video');
vid.src = url;
vid.muted = true;
vid.preload = 'metadata';
previewItem.appendChild(vid);
} else {
const icon = document.createElement('i');
icon.className = 'fa-solid fa-music';
previewItem.appendChild(icon);
}
const nameEl = document.createElement('span');
nameEl.className = 'cf-filename';
nameEl.textContent = file.name;
previewItem.appendChild(nameEl);
const removeBtn = document.createElement('button');
removeBtn.className = 'cf-remove-btn';
removeBtn.title = removeLabel;
removeBtn.innerHTML = '<i class="fa-solid fa-xmark"></i>';
previewItem.appendChild(removeBtn);
}
} else {
textarea.value = textarea.value.replace(placeholder, '');
if (previewItem) previewItem.remove();
alert('Upload error: ' + (json.msg || 'Unknown error'));
}
} catch (err) {
textarea.value = textarea.value.replace(placeholder, '');
if (previewItem) previewItem.remove();
alert('Upload failed: ' + err.message);
}
}
fileInput.value = '';
});
this.container.addEventListener('click', async (e) => {
_f0ckDebug('[DEBUG] Click on container:', e.target);
const target = e.target;
@@ -2106,6 +2238,32 @@ class CommentSystem {
return;
}
// Attach file button
if (target.matches('.comment-attach-btn') || target.closest('.comment-attach-btn')) {
const wrap = target.closest('.comment-input');
const fileInput = wrap?.querySelector('.comment-file-input');
if (fileInput) fileInput.click();
return;
}
// Remove file preview + strip URL from textarea
if (target.matches('.cf-remove-btn') || target.closest('.cf-remove-btn')) {
const previewItem = target.closest('.cf-preview-item');
if (previewItem) {
const url = previewItem.dataset.url;
if (url) {
const wrap = previewItem.closest('.comment-input');
const textarea = wrap?.querySelector('textarea');
if (textarea) {
// Remove the URL and any surrounding newline
textarea.value = textarea.value.replace(new RegExp('\\n?' + url.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&') + '\\n?'), '\n').replace(/^\n|\n$/g, '');
}
}
previewItem.remove();
}
return;
}
// Submit Comment
if (target.matches('.submit-comment')) {
this.handleSubmit(e);
@@ -2467,6 +2625,8 @@ class CommentSystem {
counter.textContent = `0 / ${counter.dataset.max}`;
counter.classList.remove('near-limit', 'at-limit');
}
const fpArea = wrap.querySelector('.comment-file-preview');
if (fpArea) fpArea.innerHTML = '';
}
// Notify the right sidebar that a new comment was posted (silent refresh)

View File

@@ -7831,6 +7831,15 @@ document.addEventListener('DOMContentLoaded', () => {
} else {
openImageModal(elfe.href);
}
return;
}
// Comment embedded images → open in image modal
const commentImg = e.target.closest('.comment-content img, .comment-attachments img');
if (commentImg) {
e.preventDefault();
e.stopPropagation();
openImageModal(commentImg.src);
}
});
};

View File

@@ -128,7 +128,8 @@
const hostsRegexPart = allowedHosts.join('|');
// "Safe non-whitespace" stops at protocol boundaries so concatenated URLs aren't merged.
const safeS = `(?:(?!https?:\\/\\/)\\S)`;
const imageRegex = new RegExp(`(?<![\\(\\[])((?:https?:\\/\\/)?(?:${hostsRegexPart})(?:\\/${safeS}+\\.(?:jpg|jpeg|png|gif|webp)(?:\\?${safeS}+)?))(?![\\)\\]])`, 'gi');
const domainOrRelative = `(?:(?:https?:\\/\\/|\\/\\/)?(?:${hostsRegexPart})|(?<!\\S)(?=\\/[a-zA-Z0-9_\\-]))`;
const imageRegex = new RegExp(`(?<![\\(\\[])(${domainOrRelative}(?:\\/${safeS}+\\.(?:jpg|jpeg|png|gif|webp)(?:\\?${safeS}+)?))(?![\\)\\]])`, 'gi');
const mentionRegex = /(?<!\[)@([a-zA-Z0-9_\-\.]+)(?!\])|\[@([^\]]+)\]/g;
const renderer = new marked.Renderer();