#12 first commit
This commit is contained in:
@@ -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 */
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user