updating from dev
This commit is contained in:
@@ -290,7 +290,7 @@
|
||||
old.innerText = res.tag.trim();
|
||||
break;
|
||||
default:
|
||||
console.log(res);
|
||||
window.f0ckDebug(res);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
||||
const showModal = () => {
|
||||
if (!dragModal) return;
|
||||
dragModal.classList.add('show');
|
||||
document.body.classList.add('modal-open');
|
||||
// Reset scroll position so it always starts at the top
|
||||
dragModal.scrollTop = 0;
|
||||
const modalContent = dragModal.querySelector('.modal-content');
|
||||
@@ -89,6 +90,7 @@
|
||||
// Modal Close
|
||||
dragModalClose.onclick = () => {
|
||||
dragModal.classList.remove('show');
|
||||
document.body.classList.remove('modal-open');
|
||||
if (uploader && uploader.reset) {
|
||||
uploader.reset();
|
||||
}
|
||||
|
||||
@@ -40,12 +40,92 @@ window.cancelAnimFrame = (function () {
|
||||
} catch(e) { console.error('Visit tracking error:', e); }
|
||||
};
|
||||
|
||||
window.applyThumbCacheBust = (bgUrlStr) => {
|
||||
if (!bgUrlStr) return bgUrlStr;
|
||||
try {
|
||||
const bustedStr = localStorage.getItem('bustedThumbs');
|
||||
if (!bustedStr) return bgUrlStr;
|
||||
const busted = JSON.parse(bustedStr);
|
||||
const match = bgUrlStr.match(/\/t\/(\d+)(?:_blur)?\.webp/);
|
||||
if (match) {
|
||||
const id = match[1];
|
||||
if (busted[id]) {
|
||||
const url = new URL(bgUrlStr, window.location.origin);
|
||||
url.searchParams.set('t', busted[id]);
|
||||
return url.pathname + url.search;
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
return bgUrlStr;
|
||||
};
|
||||
|
||||
/**
|
||||
* Forcefully refreshes all thumbnail occurrences for a specific item in the DOM.
|
||||
* Handles grid items (data-bg), images (src), and the background canvas.
|
||||
*/
|
||||
window.refreshItemThumbnails = (itemId, timestamp = Date.now()) => {
|
||||
if (!itemId) return;
|
||||
const idStr = String(itemId);
|
||||
|
||||
// Update localStorage so future navigations use the new timestamp
|
||||
try {
|
||||
const bustedStr = localStorage.getItem('bustedThumbs');
|
||||
const busted = bustedStr ? JSON.parse(bustedStr) : {};
|
||||
busted[idStr] = timestamp;
|
||||
const keys = Object.keys(busted);
|
||||
if (keys.length > 50) delete busted[keys[0]];
|
||||
localStorage.setItem('bustedThumbs', JSON.stringify(busted));
|
||||
} catch(e) {}
|
||||
|
||||
// Clear grid cache to force fresh render on next navigation
|
||||
if (typeof gridCacheMap !== 'undefined') gridCacheMap.clear();
|
||||
|
||||
// Update elements with data-bg (grid items).
|
||||
// We look for any data-bg or inline style containing the thumbnail path for this ID.
|
||||
document.querySelectorAll(`[data-bg*="/t/${idStr}.webp"], [data-bg*="/t/${idStr}_blur.webp"], [style*="/t/${idStr}.webp"], [style*="/t/${idStr}_blur.webp"]`).forEach(el => {
|
||||
// If it has data-bg, update it (this handles lazy-thumb logic)
|
||||
if (el.dataset.bg) {
|
||||
el.dataset.bg = window.applyThumbCacheBust(el.dataset.bg);
|
||||
}
|
||||
// If it's already showing the background, update the style directly
|
||||
if (el.style.backgroundImage || el.getAttribute('style')?.includes('background-image')) {
|
||||
const currentStyle = el.getAttribute('style') || '';
|
||||
// Match url(...) contents
|
||||
const newStyle = currentStyle.replace(/url\(['"]?([^'"]+)['"]?\)/g, (match, p1) => {
|
||||
if (p1.includes(`/t/${idStr}.webp`) || p1.includes(`/t/${idStr}_blur.webp`)) {
|
||||
return `url('${window.applyThumbCacheBust(p1)}')`;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
el.setAttribute('style', newStyle);
|
||||
}
|
||||
});
|
||||
|
||||
// Update actual img tags
|
||||
document.querySelectorAll(`img[src*="/t/${idStr}.webp"], img[src*="/t/${idStr}_blur.webp"]`).forEach(el => {
|
||||
try {
|
||||
const url = new URL(el.src, window.location.origin);
|
||||
url.searchParams.set('t', timestamp);
|
||||
el.src = url.pathname + url.search;
|
||||
} catch(e) {}
|
||||
});
|
||||
|
||||
// Refresh background canvas if it matches the current item
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const numParts = pathParts.filter(s => /^\d+$/.test(s));
|
||||
const currentId = numParts.length > 0 ? numParts[numParts.length - 1] : null;
|
||||
if (currentId === idStr && window.initBackground) {
|
||||
window.initBackground();
|
||||
}
|
||||
};
|
||||
|
||||
let lazyObserver;
|
||||
window.initLazyLoading = () => {
|
||||
if (!('IntersectionObserver' in window)) {
|
||||
document.querySelectorAll('.lazy-thumb').forEach(thumb => {
|
||||
if (thumb.dataset.bg) {
|
||||
thumb.style.backgroundImage = `url('${thumb.dataset.bg}')`;
|
||||
const finalBg = window.applyThumbCacheBust(thumb.dataset.bg);
|
||||
thumb.style.backgroundImage = `url('${finalBg}')`;
|
||||
thumb.classList.remove('lazy-thumb');
|
||||
}
|
||||
});
|
||||
@@ -57,8 +137,9 @@ window.cancelAnimFrame = (function () {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const thumb = entry.target;
|
||||
const bg = thumb.dataset.bg;
|
||||
let bg = thumb.dataset.bg;
|
||||
if (bg && !thumb.classList.contains('loaded')) {
|
||||
bg = window.applyThumbCacheBust(bg);
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
thumb.style.backgroundImage = `url('${bg}')`;
|
||||
@@ -293,11 +374,61 @@ window.cancelAnimFrame = (function () {
|
||||
if (!modal) return;
|
||||
if (modal === loginModal) switchModalView(view);
|
||||
modal.style.display = 'flex';
|
||||
document.body.classList.add('modal-open');
|
||||
if (visitorMenu) visitorMenu.classList.remove('show');
|
||||
};
|
||||
|
||||
const closeModal = (modal) => {
|
||||
if (modal) modal.style.display = 'none';
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
document.body.classList.remove('modal-open');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Surgical cleanup of scroll-lock state and modal visibility.
|
||||
* Used during AJAX navigation to ensure the UI remains interactive.
|
||||
*/
|
||||
window.resetGlobalScrollState = () => {
|
||||
document.body.classList.remove('modal-open');
|
||||
document.documentElement.classList.remove('modal-open');
|
||||
document.body.style.overflow = '';
|
||||
document.body.style.height = '';
|
||||
document.documentElement.style.overflow = '';
|
||||
document.documentElement.style.height = '';
|
||||
const pw = document.querySelector('.pagewrapper');
|
||||
if (pw) {
|
||||
pw.style.overflow = '';
|
||||
pw.style.height = '';
|
||||
}
|
||||
};
|
||||
|
||||
window.hideAllModals = () => {
|
||||
const modalIds = [
|
||||
'login-modal', 'register-modal', 'forgot-modal', 'reset-modal',
|
||||
'report-modal', 'halls-modal', 'metadata-modal', 'warning-modal',
|
||||
'shortcuts-modal', 'upload-drag-modal', 'excluded-tags-overlay',
|
||||
'content-warning-modal', 'gchat-img-modal', 'image-modal'
|
||||
];
|
||||
modalIds.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.classList.remove('show', 'visible');
|
||||
// If the modal uses CSS classes for visibility, we must clear the inline display
|
||||
// to allow those classes to work later. For others, we force display: none.
|
||||
if (['upload-drag-modal', 'image-modal', 'gchat-img-modal', 'excluded-tags-overlay'].includes(id)) {
|
||||
el.style.display = '';
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
// Also handle class-based modals if any
|
||||
document.querySelectorAll('.modal-overlay, .modal-backdrop').forEach(el => {
|
||||
el.classList.remove('show', 'visible');
|
||||
// Do NOT set display: none here as it might override CSS-based visibility
|
||||
// for modals that use the classes we just removed.
|
||||
});
|
||||
};
|
||||
|
||||
if (loginModal) {
|
||||
@@ -512,6 +643,8 @@ window.cancelAnimFrame = (function () {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (registerBtn && registerModal) {
|
||||
registerBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
@@ -844,7 +977,9 @@ window.cancelAnimFrame = (function () {
|
||||
}
|
||||
// For audio-only items with no thumbnail, canvas stays blank (nothing to draw)
|
||||
};
|
||||
thumb.src = `/t/${itemId}.webp`;
|
||||
let newSrc = `/t/${itemId}.webp`;
|
||||
if (window.applyThumbCacheBust) newSrc = window.applyThumbCacheBust(newSrc);
|
||||
thumb.src = newSrc;
|
||||
} else if (isDrawable) {
|
||||
// No item ID — fall back to waiting for the main image
|
||||
if (elem.complete) {
|
||||
@@ -1016,8 +1151,7 @@ window.cancelAnimFrame = (function () {
|
||||
if (cwModal) {
|
||||
if (!localStorage.getItem('content_warning_accepted')) {
|
||||
cwModal.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden'; // Prevent scrolling on body
|
||||
document.documentElement.style.overflow = 'hidden'; // Prevent scrolling on html
|
||||
document.body.classList.add('modal-open');
|
||||
}
|
||||
|
||||
const acceptBtn = document.getElementById('cw-accept');
|
||||
@@ -1027,8 +1161,7 @@ window.cancelAnimFrame = (function () {
|
||||
acceptBtn.addEventListener('click', () => {
|
||||
localStorage.setItem('content_warning_accepted', 'true');
|
||||
cwModal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
document.documentElement.style.overflow = '';
|
||||
document.body.classList.remove('modal-open');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1079,7 +1212,7 @@ window.cancelAnimFrame = (function () {
|
||||
const applyRuffleKeepAlive = () => {
|
||||
if (ruffleKeepAliveApplied) return;
|
||||
|
||||
console.log("[Ruffle] Registering background keep-alive patches (Browser Level)...");
|
||||
window.f0ckDebug("[Ruffle] Registering background keep-alive patches (Browser Level)...");
|
||||
|
||||
try {
|
||||
const docProto = Object.getPrototypeOf(document);
|
||||
@@ -1422,6 +1555,11 @@ window.cancelAnimFrame = (function () {
|
||||
|
||||
const loadPageAjax = async (url, replace = true, options = {}) => {
|
||||
if (isNavigating) return;
|
||||
|
||||
// Immediately restore scrollability and hide modals
|
||||
if (window.resetGlobalScrollState) window.resetGlobalScrollState();
|
||||
if (window.hideAllModals) window.hideAllModals();
|
||||
|
||||
isNavigating = true;
|
||||
|
||||
// ── Scroller-active cleanup ──────────────────────────────────────────────
|
||||
@@ -1793,7 +1931,7 @@ window.cancelAnimFrame = (function () {
|
||||
document.body.style.height = '';
|
||||
document.body.style.minHeight = '';
|
||||
|
||||
console.log("[loadPageAjax] State synced for " + (isFullPage ? "full page" : "partial"));
|
||||
window.f0ckDebug("[loadPageAjax] State synced for " + (isFullPage ? "full page" : "partial"));
|
||||
if (window.updateMimeLabel) window.updateMimeLabel();
|
||||
}
|
||||
|
||||
@@ -2287,7 +2425,7 @@ window.cancelAnimFrame = (function () {
|
||||
m.removeAttribute('src');
|
||||
m.preload = 'none'; // Prevent further buffering
|
||||
// m.load(); // Intentionally removed: calling load() with no src can fetch current page URL
|
||||
console.log("Media aborted:", m);
|
||||
window.f0ckDebug("Media aborted:", m);
|
||||
} catch (e) { console.error("Error stopping media:", e); }
|
||||
});
|
||||
};
|
||||
@@ -2309,7 +2447,7 @@ window.cancelAnimFrame = (function () {
|
||||
|
||||
try {
|
||||
const fetchUrl = `/ajax/item/${itemid}?${params.toString()}`;
|
||||
console.log(`[updateNavForMode] mode=${mode} itemid=${itemid} → ${fetchUrl}`);
|
||||
window.f0ckDebug(`[updateNavForMode] mode=${mode} itemid=${itemid} → ${fetchUrl}`);
|
||||
const resp = await fetch(fetchUrl, { credentials: 'include' });
|
||||
if (!resp.ok) { console.warn('[updateNavForMode] bad response:', resp.status); return; }
|
||||
const data = await resp.json();
|
||||
@@ -2326,14 +2464,14 @@ window.cancelAnimFrame = (function () {
|
||||
if (livePrev && newPrev) {
|
||||
const h = newPrev.getAttribute('href');
|
||||
livePrev.setAttribute('href', h);
|
||||
console.log(`[updateNavForMode] #prev → ${h}`);
|
||||
window.f0ckDebug(`[updateNavForMode] #prev → ${h}`);
|
||||
} else {
|
||||
console.warn(`[updateNavForMode] #prev missing: live=${!!livePrev} new=${!!newPrev}`);
|
||||
}
|
||||
if (liveNext && newNext) {
|
||||
const h = newNext.getAttribute('href');
|
||||
liveNext.setAttribute('href', h);
|
||||
console.log(`[updateNavForMode] #next → ${h}`);
|
||||
window.f0ckDebug(`[updateNavForMode] #next → ${h}`);
|
||||
} else {
|
||||
console.warn(`[updateNavForMode] #next missing: live=${!!liveNext} new=${!!newNext}`);
|
||||
}
|
||||
@@ -2353,6 +2491,11 @@ window.cancelAnimFrame = (function () {
|
||||
const loadItemAjax = async (url, inheritContext = true, options = {}) => {
|
||||
|
||||
if (isNavigating) return;
|
||||
|
||||
// Immediately restore scrollability and hide modals
|
||||
if (window.resetGlobalScrollState) window.resetGlobalScrollState();
|
||||
if (window.hideAllModals) window.hideAllModals();
|
||||
|
||||
isNavigating = true;
|
||||
|
||||
// ── Scroller-active cleanup (same as loadPageAjax) ───────────────────
|
||||
@@ -2534,7 +2677,7 @@ window.cancelAnimFrame = (function () {
|
||||
|
||||
if (window.randomizeLogo) window.randomizeLogo();
|
||||
|
||||
console.log("Fetching:", ajaxUrl);
|
||||
window.f0ckDebug("Fetching:", ajaxUrl);
|
||||
const tStart = performance.now();
|
||||
const response = await fetch(ajaxUrl, { credentials: 'include' });
|
||||
const tHeaders = performance.now();
|
||||
@@ -2544,7 +2687,7 @@ window.cancelAnimFrame = (function () {
|
||||
const rawText = await response.text();
|
||||
const tBody = performance.now();
|
||||
|
||||
console.log(`[CLIENT_DEBUG] Fetch timing for ${ajaxUrl}:
|
||||
window.f0ckDebug(`[CLIENT_DEBUG] Fetch timing for ${ajaxUrl}:
|
||||
- TTFB (Headers): ${(tHeaders - tStart).toFixed(2)}ms
|
||||
- Content Download: ${(tBody - tHeaders).toFixed(2)}ms
|
||||
- Total Network: ${(tBody - tStart).toFixed(2)}ms
|
||||
@@ -2705,7 +2848,7 @@ window.cancelAnimFrame = (function () {
|
||||
// Try to extract ID from response if possible or just use itemid
|
||||
document.title = `${window.f0ckDomain} - ${itemid}`;
|
||||
if (navbar) navbar.classList.remove("pbwork");
|
||||
console.log("AJAX load complete");
|
||||
window.f0ckDebug("AJAX load complete");
|
||||
|
||||
// Notify extensions — also triggers CommentSystem init which renders comments
|
||||
document.dispatchEvent(new Event('f0ck:contentLoaded'));
|
||||
@@ -2956,8 +3099,10 @@ window.cancelAnimFrame = (function () {
|
||||
if (window.f0ckSession && window.f0ckSession.logged_in) {
|
||||
window.showFlash('Already logged in lol', 'error');
|
||||
} else {
|
||||
loadPageAjax(anyLink.href, true);
|
||||
if (pathname === '/login') openModal(loginModal, 'login');
|
||||
else openModal(registerModal);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3067,6 +3212,9 @@ window.cancelAnimFrame = (function () {
|
||||
fileInput.value = '';
|
||||
if (data.success) {
|
||||
window.flashMessage(data.msg);
|
||||
if (window.refreshItemThumbnails) {
|
||||
window.refreshItemThumbnails(currentItemId);
|
||||
}
|
||||
} else {
|
||||
window.flashError(data.msg || 'Upload failed');
|
||||
}
|
||||
@@ -3163,7 +3311,7 @@ window.cancelAnimFrame = (function () {
|
||||
const isSpecial = p.startsWith('/notifications') || p.startsWith('/tags') || p.startsWith('/user/') || p.startsWith('/subscriptions') || p.startsWith('/ranking');
|
||||
const isGridLike = url.match(/\/p\/\d+/) || url.match(/[?&]page=\d+/) || p === '/';
|
||||
|
||||
console.log("[popstate] Navigation to:", url, { isItem, isSpecial, isGridLike });
|
||||
window.f0ckDebug("[popstate] Navigation to:", url, { isItem, isSpecial, isGridLike });
|
||||
|
||||
if (isItem) {
|
||||
loadItemAjax(url, true, { skipPush: true });
|
||||
@@ -4280,6 +4428,7 @@ window.cancelAnimFrame = (function () {
|
||||
const tag = (e.target.tagName || '').toLowerCase();
|
||||
if (tag === 'input' || tag === 'textarea' || e.target.isContentEditable) return;
|
||||
if (window.location.pathname.startsWith('/abyss')) return;
|
||||
e.preventDefault();
|
||||
toggleModal(!overlay.classList.contains('visible'));
|
||||
}
|
||||
});
|
||||
@@ -4510,6 +4659,7 @@ window.cancelAnimFrame = (function () {
|
||||
};
|
||||
|
||||
window.updateXdBadgeFromScore = (itemId, score) => {
|
||||
if (window.f0ckSession && window.f0ckSession.enable_xd_score === false) return;
|
||||
updateItemPageXdBadge(itemId, score);
|
||||
updateThumbXdIndicator(itemId, score);
|
||||
};
|
||||
@@ -5143,7 +5293,7 @@ class NotificationSystem {
|
||||
fetch(`/api/notifications/active?tabId=${this.tabId}`).catch(() => {});
|
||||
// If SSE is closed, reconnect
|
||||
if (!this.es || this.es.readyState === 2) {
|
||||
console.log("[NotificationSystem] SSE was closed, reconnecting as active tab...");
|
||||
window.f0ckDebug("[NotificationSystem] SSE was closed, reconnecting as active tab...");
|
||||
this.initSSE();
|
||||
}
|
||||
};
|
||||
@@ -5195,17 +5345,19 @@ class NotificationSystem {
|
||||
});
|
||||
}
|
||||
|
||||
console.log("[NotificationSystem] Tab visible, signaling active...");
|
||||
window.f0ckDebug("[NotificationSystem] Tab visible, signaling active...");
|
||||
// If SSE died while hidden, restart it now
|
||||
if (!this.es) {
|
||||
console.log("[NotificationSystem] SSE was dead, restarting on tab visible.");
|
||||
window.f0ckDebug("[NotificationSystem] SSE was dead, restarting on tab visible.");
|
||||
this.retryCount = 0;
|
||||
this.initSSE();
|
||||
}
|
||||
if (this.pollDebounced) this.pollDebounced();
|
||||
if (this.checkForNewItems) this.checkForNewItems();
|
||||
// Catch-up on emojis if they were updated while this tab was pruned/backgrounded
|
||||
window.dispatchEvent(new CustomEvent('f0ck:emojis_updated'));
|
||||
// Note: emojis_updated dispatch was removed from here — it caused emoji cache flush +
|
||||
// async re-fetch + re-render that could fight the scroll-position restoration.
|
||||
// Emojis rarely change while a tab is hidden; SSE will deliver a targeted emojis_updated
|
||||
// event if they actually changed.
|
||||
signalActive();
|
||||
// Sync display name in case it was changed while this tab was inactive
|
||||
if (window.f0ckSession?.logged_in) this.syncDisplayName();
|
||||
@@ -5241,11 +5393,11 @@ class NotificationSystem {
|
||||
this.es.close();
|
||||
}
|
||||
|
||||
console.log(`[NotificationSystem] Initializing SSE connection (tabId: ${this.tabId})...`);
|
||||
window.f0ckDebug(`[NotificationSystem] Initializing SSE connection (tabId: ${this.tabId})...`);
|
||||
this.es = new EventSource(`/api/notifications/stream?tabId=${this.tabId}`);
|
||||
|
||||
this.es.onopen = () => {
|
||||
console.log("[NotificationSystem] SSE connection established");
|
||||
window.f0ckDebug("[NotificationSystem] SSE connection established");
|
||||
this.retryCount = 0;
|
||||
document.documentElement.dataset.sseReady = '1';
|
||||
document.dispatchEvent(new CustomEvent('f0ck:sse_ready'));
|
||||
@@ -5254,16 +5406,17 @@ class NotificationSystem {
|
||||
this.es.onmessage = (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
console.log(`[SSE] Received message:`, data.type);
|
||||
window.f0ckDebug(`[SSE] Received message:`, data.type);
|
||||
if (data.type === 'notify') {
|
||||
this.pollDebounced();
|
||||
const dnd = window.f0ckSession?.do_not_disturb === true;
|
||||
// Haptic feedback on mobile (supported: Chrome for Android, not iOS)
|
||||
if (navigator.vibrate) navigator.vibrate([200, 80, 200]);
|
||||
if (!dnd && navigator.vibrate) navigator.vibrate([200, 80, 200]);
|
||||
// Live Grid Highlight
|
||||
if (data.data && data.data.item_id) {
|
||||
const itemId = data.data.item_id;
|
||||
const notifType = data.data.type;
|
||||
console.log(`[SSE] Live notification for item ${itemId} (type: ${notifType})`);
|
||||
window.f0ckDebug(`[SSE] Live notification for item ${itemId} (type: ${notifType})`);
|
||||
|
||||
// System notifications (deletion, deny, reports) require explicit acknowledgment —
|
||||
// never auto-mark them as read just because the user is viewing that item.
|
||||
@@ -5273,7 +5426,7 @@ class NotificationSystem {
|
||||
// (they are live on the thread, so no need to show a badge/highlight)
|
||||
const currentPath = window.location.pathname;
|
||||
if (!isSystemNotif && (currentPath === `/${itemId}` || currentPath === `/${itemId}/`)) {
|
||||
console.log(`[SSE] Notification for current item ${itemId} — auto-marking as read`);
|
||||
window.f0ckDebug(`[SSE] Notification for current item ${itemId} — auto-marking as read`);
|
||||
fetch(`/api/notifications/item/${itemId}/read`, {
|
||||
method: 'POST',
|
||||
keepalive: true
|
||||
@@ -5307,13 +5460,13 @@ class NotificationSystem {
|
||||
}
|
||||
this.handleActivity(data.data);
|
||||
} else if (data.type === 'tags') {
|
||||
console.log(`[SSE] Tag update received for item ${data.data?.item_id}`);
|
||||
window.f0ckDebug(`[SSE] Tag update received for item ${data.data?.item_id}`);
|
||||
this.handleTagsUpdate(data.data);
|
||||
} else if (data.type === 'favorites') {
|
||||
console.log(`[SSE] Favorite update received for item ${data.data?.item_id}`);
|
||||
window.f0ckDebug(`[SSE] Favorite update received for item ${data.data?.item_id}`);
|
||||
this.handleFavoritesUpdate(data.data);
|
||||
} else if (data.type === 'comments') {
|
||||
console.log(`[SSE] Comment update received:`, data.data);
|
||||
window.f0ckDebug(`[SSE] Comment update received:`, data.data);
|
||||
if (data.data.type === 'comment') {
|
||||
// New comment posted — update xD badge from server-authoritative score
|
||||
if (typeof data.data.xd_score === 'number') {
|
||||
@@ -5339,21 +5492,26 @@ class NotificationSystem {
|
||||
window.dispatchEvent(new CustomEvent('f0ck:comment_edited', { detail: data.data }));
|
||||
}
|
||||
} else if (data.type === 'emojis_updated') {
|
||||
console.log("[SSE] Emojis updated, refreshing caches...");
|
||||
window.f0ckDebug("[SSE] Emojis updated, refreshing caches...");
|
||||
this.loadEmojis();
|
||||
window.dispatchEvent(new CustomEvent('f0ck:emojis_updated'));
|
||||
} else if (data.type === 'motd') {
|
||||
console.log(`[SSE] MOTD update received:`, data.data.motd);
|
||||
window.f0ckDebug(`[SSE] MOTD update received:`, data.data.motd);
|
||||
if (typeof window.updateMotdUI === 'function') {
|
||||
window.updateMotdUI(data.data.motd);
|
||||
}
|
||||
} else if (data.type === 'rethumb') {
|
||||
window.f0ckDebug(`[SSE] Rethumb update received for item ${data.data?.item_id}`);
|
||||
if (data.data && data.data.item_id && window.refreshItemThumbnails) {
|
||||
window.refreshItemThumbnails(data.data.item_id);
|
||||
}
|
||||
} else if (data.type === 'new_item') {
|
||||
console.log(`[SSE] New item received:`, data.data);
|
||||
window.f0ckDebug(`[SSE] New item received:`, data.data);
|
||||
this.handleNewItem(data.data);
|
||||
} else if (data.type === 'delete_item') {
|
||||
const delId = data.data?.id;
|
||||
if (!delId) return;
|
||||
console.log(`[SSE] Item deleted: ${delId}`);
|
||||
window.f0ckDebug(`[SSE] Item deleted: ${delId}`);
|
||||
|
||||
// Remove from main grid — a.thumb is the anchor, li is its parent card
|
||||
const thumb = document.querySelector(`a.thumb[href$="/${delId}"], a.lazy-thumb[href$="/${delId}"]`);
|
||||
@@ -5389,22 +5547,23 @@ class NotificationSystem {
|
||||
// Remove from sidebar activity if present
|
||||
document.querySelectorAll(`#sidebar-activity-container [data-item="${delId}"]`).forEach(el => el.remove());
|
||||
} else if (data.type === 'emojis_updated') {
|
||||
console.log(`[SSE] Emoji update event received`);
|
||||
window.f0ckDebug(`[SSE] Emoji update event received`);
|
||||
if (window.commentSystem && typeof window.commentSystem.loadEmojis === 'function') {
|
||||
window.commentSystem.loadEmojis();
|
||||
}
|
||||
// Global dispatch for other listeners (e.g. Admin Dashboard)
|
||||
document.dispatchEvent(new Event('f0ck:emojis_updated'));
|
||||
} else if (data.type === 'warning') {
|
||||
console.log(`[SSE] Warning received:`, data.data);
|
||||
window.f0ckDebug(`[SSE] Warning received:`, data.data);
|
||||
const warningModal = document.getElementById('warning-modal');
|
||||
if (warningModal) {
|
||||
document.getElementById('warning-reason').textContent = data.data.reason;
|
||||
document.getElementById('warning-id').value = data.data.warning_id;
|
||||
warningModal.style.display = 'flex';
|
||||
document.body.classList.add('modal-open');
|
||||
}
|
||||
} else if (data.type === 'private_message') {
|
||||
console.log(`[SSE] Private message received from user ${data.data?.sender_id}`);
|
||||
window.f0ckDebug(`[SSE] Private message received from user ${data.data?.sender_id}`);
|
||||
// Haptic feedback for DMs (distinct double-pulse pattern)
|
||||
if (navigator.vibrate) navigator.vibrate([120, 60, 120]);
|
||||
// Dispatch event for messages.js thread live-update
|
||||
@@ -5432,6 +5591,12 @@ class NotificationSystem {
|
||||
} else if (data.type === 'profile_update') {
|
||||
const { display_name, user } = data.data;
|
||||
this.applyDisplayNameUpdate(display_name, user);
|
||||
// Sync preferences to global session object for client-side gating (like DND)
|
||||
if (window.f0ckSession && data.data.user_id === window.f0ckSession.id) {
|
||||
for (const key in data.data) {
|
||||
if (key !== 'user_id') window.f0ckSession[key] = data.data[key];
|
||||
}
|
||||
}
|
||||
} else if (data.type === 'global_chat') {
|
||||
document.dispatchEvent(new CustomEvent('f0ck:global_chat', { detail: data.data }));
|
||||
} else if (data.type === 'global_chat_clear') {
|
||||
@@ -5505,14 +5670,14 @@ class NotificationSystem {
|
||||
|
||||
// If tab is hidden, don't retry now — visibilitychange will restart SSE when visible again
|
||||
if (document.hidden) {
|
||||
console.log("[NotificationSystem] Tab hidden, deferring SSE retry until visible.");
|
||||
window.f0ckDebug("[NotificationSystem] Tab hidden, deferring SSE retry until visible.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Exponential backoff, capped at 30s
|
||||
const delay = Math.min(Math.pow(2, this.retryCount) * 1000, 30000);
|
||||
if (this.retryCount < this.maxRetries) {
|
||||
console.log(`[NotificationSystem] Retrying SSE in ${delay}ms... (attempt ${this.retryCount + 1}/${this.maxRetries})`);
|
||||
window.f0ckDebug(`[NotificationSystem] Retrying SSE in ${delay}ms... (attempt ${this.retryCount + 1}/${this.maxRetries})`);
|
||||
setTimeout(() => this.initSSE(), delay);
|
||||
this.retryCount++;
|
||||
} else {
|
||||
@@ -5618,7 +5783,7 @@ class NotificationSystem {
|
||||
|
||||
// Handle "Mark as Read"
|
||||
if (link.dataset.id && link.classList.contains('unread')) {
|
||||
console.log(`[NotificationSystem] Marking ${link.dataset.id} as read...`);
|
||||
window.f0ckDebug(`[NotificationSystem] Marking ${link.dataset.id} as read...`);
|
||||
// Fire and forget (keepalive ensures it survives navigation)
|
||||
fetch(`/api/notifications/${link.dataset.id}/read`, {
|
||||
method: 'POST',
|
||||
@@ -5664,6 +5829,10 @@ class NotificationSystem {
|
||||
if (href && href !== '#') {
|
||||
e.preventDefault();
|
||||
|
||||
// Immediately restore scrollability and hide modals for better UX
|
||||
if (window.resetGlobalScrollState) window.resetGlobalScrollState();
|
||||
if (window.hideAllModals) window.hideAllModals();
|
||||
|
||||
// Close dropdown
|
||||
if (this.isOpen) this.close();
|
||||
|
||||
@@ -5766,7 +5935,7 @@ class NotificationSystem {
|
||||
if (ids.length === 0) return;
|
||||
const maxId = Math.max(...ids);
|
||||
|
||||
console.log(`[NotificationSystem] Checking for items newer than ${maxId}...`);
|
||||
window.f0ckDebug(`[NotificationSystem] Checking for items newer than ${maxId}...`);
|
||||
|
||||
try {
|
||||
// Build filters from URL
|
||||
@@ -5806,7 +5975,7 @@ class NotificationSystem {
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success && data.html) {
|
||||
console.log(`[NotificationSystem] Loaded new items for grid.`);
|
||||
window.f0ckDebug(`[NotificationSystem] Loaded new items for grid.`);
|
||||
const temp = document.createElement('div');
|
||||
temp.innerHTML = Sanitizer.clean(data.html);
|
||||
|
||||
@@ -5911,7 +6080,7 @@ class NotificationSystem {
|
||||
tabNotifs.forEach(n => {
|
||||
const existing = historyContainer.querySelector(`.notif-item[data-id="${n.id}"]`);
|
||||
if (!existing) {
|
||||
console.log("[NotificationSystem] Adding new item to history:", n.id);
|
||||
window.f0ckDebug("[NotificationSystem] Adding new item to history:", n.id);
|
||||
const html = this.renderHistoryItem(n);
|
||||
const temp = document.createElement('div');
|
||||
temp.innerHTML = Sanitizer.clean(html);
|
||||
@@ -5919,7 +6088,7 @@ class NotificationSystem {
|
||||
node.classList.add('new-item-fade');
|
||||
historyContainer.prepend(node);
|
||||
} else {
|
||||
console.log("[NotificationSystem] Item already exists:", n.id);
|
||||
window.f0ckDebug("[NotificationSystem] Item already exists:", n.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -6177,10 +6346,10 @@ class NotificationSystem {
|
||||
const idLink = document.querySelector('.id-link');
|
||||
const currentId = idLink ? parseInt(idLink.innerText) : null;
|
||||
|
||||
console.log(`[NotificationSystem] Processing tag update for #${data.item_id}. Current view is #${currentId}`);
|
||||
window.f0ckDebug(`[NotificationSystem] Processing tag update for #${data.item_id}. Current view is #${currentId}`);
|
||||
|
||||
if (currentId !== parseInt(data.item_id)) {
|
||||
console.log("[NotificationSystem] Item ID mismatch, ignoring update.");
|
||||
window.f0ckDebug("[NotificationSystem] Item ID mismatch, ignoring update.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6192,11 +6361,11 @@ class NotificationSystem {
|
||||
|
||||
// DO NOT re-render if the user is currently typing a new tag (has an active input)
|
||||
if (tagsContainer.querySelector('input')) {
|
||||
console.log("[NotificationSystem] Live Tag Update deferred - User is currently typing.");
|
||||
window.f0ckDebug("[NotificationSystem] Live Tag Update deferred - User is currently typing.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[NotificationSystem] Re-rendering tags for item:", data.item_id);
|
||||
window.f0ckDebug("[NotificationSystem] Re-rendering tags for item:", data.item_id);
|
||||
|
||||
const inner = tagsContainer.querySelector('.tags-inner') || tagsContainer;
|
||||
|
||||
@@ -6216,7 +6385,7 @@ class NotificationSystem {
|
||||
const isAdminBySession = !!(window.f0ckSession?.is_admin || window.f0ckSession?.is_moderator);
|
||||
const hasSession = !!window.f0ckSession;
|
||||
|
||||
console.log(`[NotificationSystem] Rendering ${data.tags.length} tags. isAdmin: ${isAdminBySession}, hasSession: ${hasSession}`);
|
||||
window.f0ckDebug(`[NotificationSystem] Rendering ${data.tags.length} tags. isAdmin: ${isAdminBySession}, hasSession: ${hasSession}`);
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
data.tags.forEach(tag => {
|
||||
@@ -6256,7 +6425,7 @@ class NotificationSystem {
|
||||
const favsContainer = document.querySelector('#favs');
|
||||
if (!favsContainer) return;
|
||||
|
||||
console.log("[NotificationSystem] Live Favorite Update for item:", data.item_id);
|
||||
window.f0ckDebug("[NotificationSystem] Live Favorite Update for item:", data.item_id);
|
||||
|
||||
// Sync the heart icon for the current user
|
||||
const currentUser = window.f0ckSession?.user?.toLowerCase();
|
||||
@@ -6384,7 +6553,7 @@ class NotificationSystem {
|
||||
// 3. Handle Activity Feed
|
||||
const activityContainer = document.getElementById('activity-container');
|
||||
if (activityContainer) {
|
||||
console.log("[NotificationSystem] New Activity:", data);
|
||||
window.f0ckDebug("[NotificationSystem] New Activity:", data);
|
||||
const html = this.renderActivityItem(data);
|
||||
const temp = document.createElement('div');
|
||||
temp.innerHTML = Sanitizer.clean(html);
|
||||
@@ -6965,6 +7134,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const intervals = [
|
||||
{ unit: 'year', seconds: 31536000 },
|
||||
{ unit: 'month', seconds: 2592000 },
|
||||
{ unit: 'week', seconds: 604800 },
|
||||
{ unit: 'day', seconds: 86400 },
|
||||
{ unit: 'hour', seconds: 3600 },
|
||||
{ unit: 'minute', seconds: 60 },
|
||||
@@ -6975,13 +7145,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const count = Math.floor(seconds / interval.seconds);
|
||||
if (count >= 1) {
|
||||
try {
|
||||
// Force fallback for custom/unrecognized locales like 'zange'
|
||||
if (lang === 'zange') throw new Error('Force fallback');
|
||||
const rtf = new Intl.RelativeTimeFormat(lang, { numeric: 'auto' });
|
||||
return rtf.format(-count, interval.unit);
|
||||
} catch (e) {
|
||||
// Intl not available — fall back to i18n strings
|
||||
const key = count === 1 ? `timeago_${interval.unit}` : `timeago_${interval.unit}s`;
|
||||
const tpl = i18n[key] || `{n} ${interval.unit}${count !== 1 ? 's' : ''}`;
|
||||
const timeStr = tpl.replace('{n}', count);
|
||||
const timeStr = tpl.replace('{n}', count).replace('{s}', count !== 1 ? 's' : '');
|
||||
const agoTpl = i18n.timeago_ago || '{t} ago';
|
||||
return agoTpl.replace('{t}', timeStr);
|
||||
}
|
||||
@@ -7307,6 +7479,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
reportReason.value = '';
|
||||
reportError.textContent = '';
|
||||
reportModal.style.display = 'flex';
|
||||
document.body.classList.add('modal-open');
|
||||
}
|
||||
|
||||
const commentBtn = e.target.closest('.report-comment-btn');
|
||||
@@ -7318,6 +7491,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
reportReason.value = '';
|
||||
reportError.textContent = '';
|
||||
reportModal.style.display = 'flex';
|
||||
document.body.classList.add('modal-open');
|
||||
}
|
||||
|
||||
const userBtn = e.target.closest('.report-user-btn'); // for future
|
||||
@@ -7329,11 +7503,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
reportReason.value = '';
|
||||
reportError.textContent = '';
|
||||
reportModal.style.display = 'flex';
|
||||
document.body.classList.add('modal-open');
|
||||
}
|
||||
|
||||
// Close logic
|
||||
if (e.target.matches('#report-cancel')) {
|
||||
reportModal.style.display = 'none';
|
||||
document.body.classList.remove('modal-open');
|
||||
}
|
||||
|
||||
// Submit logic
|
||||
@@ -7392,6 +7568,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById('warning-reason').textContent = warning.reason;
|
||||
document.getElementById('warning-id').value = warning.id;
|
||||
warningModal.style.display = 'flex';
|
||||
document.body.classList.add('modal-open');
|
||||
}
|
||||
}
|
||||
} catch (e) { console.error('Error fetching warnings', e); }
|
||||
@@ -7414,6 +7591,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
document.getElementById('warning-modal').style.display = 'none';
|
||||
document.body.classList.remove('modal-open');
|
||||
// Check for more warnings
|
||||
checkWarnings();
|
||||
} else {
|
||||
@@ -8128,6 +8306,7 @@ if (navigator.vibrate) {
|
||||
const noResults = document.getElementById('metadata-no-results');
|
||||
|
||||
modal.style.display = 'flex';
|
||||
document.body.classList.add('modal-open');
|
||||
loading.style.display = 'block';
|
||||
resultsCont.style.display = 'none';
|
||||
error.style.display = 'none';
|
||||
@@ -8136,6 +8315,7 @@ if (navigator.vibrate) {
|
||||
|
||||
const close = () => {
|
||||
modal.style.display = 'none';
|
||||
document.body.classList.remove('modal-open');
|
||||
document.removeEventListener('keydown', onEsc);
|
||||
window.removeEventListener('pjax:start', onNav);
|
||||
document.removeEventListener('f0ck:contentLoaded', onContentLoaded);
|
||||
@@ -8365,4 +8545,9 @@ if (navigator.vibrate) {
|
||||
}
|
||||
}
|
||||
});
|
||||
// Ensure any navigation event restores the scroll state
|
||||
window.addEventListener('pjax:start', () => {
|
||||
if (window.resetGlobalScrollState) window.resetGlobalScrollState();
|
||||
if (window.hideAllModals) window.hideAllModals();
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -692,7 +692,7 @@
|
||||
// firefox mobile check
|
||||
const isFirefoxMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent) && /firefox/i.test(navigator.userAgent);
|
||||
if (isFirefoxMobile) {
|
||||
console.log("Firefox Mobile detected, disabling Flash Yank script.");
|
||||
window.f0ckDebug("Firefox Mobile detected, disabling Flash Yank script.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
const MAX_VISIBLE_MSGS = 100;
|
||||
const RATE_LIMIT_MS = 800;
|
||||
|
||||
let isMinimized = localStorage.getItem('f0ck_chat_minimized') === '1';
|
||||
let isMinimized = localStorage.getItem('f0ck_chat_minimized') !== '0';
|
||||
let isClosed = localStorage.getItem('f0ck_chat_closed') === '1';
|
||||
let lastSent = 0;
|
||||
let customEmojis = null; // name → url
|
||||
@@ -224,6 +224,13 @@
|
||||
`</div>` +
|
||||
`</a>`;
|
||||
});
|
||||
|
||||
// 6d.1 Vocaroo URLs → iframe embed
|
||||
const vocarooRegex = /https?:\/\/(?:www\.)?(?:voca\.ro|vocaroo\.com)\/([a-zA-Z0-9_-]+)[^\s]*/gi;
|
||||
html = html.replace(vocarooRegex, (match, vocarooId) => {
|
||||
if (['upload', 'contact', 'privacy', 'tos', 'about'].includes(vocarooId.toLowerCase())) return match;
|
||||
return `<span class="vocaroo-embed-wrap gchat-embed-audio"><iframe src="https://vocaroo.com/embed/${vocarooId}?autoplay=0" width="300" height="60" frameborder="0" allow="autoplay"></iframe></span>`;
|
||||
});
|
||||
|
||||
// 6d.5 Same-site item page links → post preview card (resolved async)
|
||||
// Only catches /digits paths — direct media file URLs are handled by 6a-6c & 6e.
|
||||
@@ -1133,8 +1140,18 @@
|
||||
panel.style.top = newY + 'px';
|
||||
};
|
||||
const onEnd = () => {
|
||||
const curY = parseInt(panel.style.top);
|
||||
localStorage.setItem('f0ck_chat_float_x', parseInt(panel.style.left));
|
||||
localStorage.setItem('f0ck_chat_float_y', parseInt(panel.style.top));
|
||||
localStorage.setItem('f0ck_chat_float_y', curY);
|
||||
|
||||
if (isMinimized) {
|
||||
const topBound = getTopBound();
|
||||
const bottomBound = window.innerHeight;
|
||||
const distToTop = Math.abs(curY - topBound);
|
||||
const distToBottom = Math.abs(bottomBound - (curY + 42));
|
||||
localStorage.setItem('f0ck_chat_anchor_top', distToTop < distToBottom ? '1' : '0');
|
||||
}
|
||||
|
||||
document.removeEventListener('mousemove', mouseMove);
|
||||
document.removeEventListener('mouseup', mouseEnd);
|
||||
document.removeEventListener('touchmove', touchMove);
|
||||
@@ -1169,28 +1186,59 @@
|
||||
// Minimize toggle — in float mode anchor to bottom edge in both directions
|
||||
function toggleMinimized() {
|
||||
const willExpand = isMinimized; // about to expand
|
||||
if (!willExpand && isFloating) {
|
||||
// About to minimize: capture bottom edge BEFORE shrinking
|
||||
if (isFloating) {
|
||||
const r = panel.getBoundingClientRect();
|
||||
const curBottom = r.top + r.height;
|
||||
setMinimized(true);
|
||||
// After CSS applies 42px height, shift top so bottom edge is preserved
|
||||
requestAnimationFrame(() => {
|
||||
const newTop = Math.min(window.innerHeight - 42, Math.max(getTopBound(), curBottom - 42));
|
||||
panel.style.top = newTop + 'px';
|
||||
localStorage.setItem('f0ck_chat_float_y', newTop);
|
||||
});
|
||||
} else {
|
||||
setMinimized(!isMinimized);
|
||||
if (willExpand && isFloating) {
|
||||
// Expanding: shift top UP to maintain bottom edge
|
||||
const topBound = getTopBound();
|
||||
const bottomBound = window.innerHeight;
|
||||
|
||||
if (!willExpand) {
|
||||
// About to minimize: decide anchor based on proximity
|
||||
const distToTop = Math.abs(r.top - topBound);
|
||||
const distToBottom = Math.abs(bottomBound - (r.top + r.height));
|
||||
const anchorTop = distToTop < distToBottom;
|
||||
localStorage.setItem('f0ck_chat_anchor_top', anchorTop ? '1' : '0');
|
||||
|
||||
const curTop = r.top;
|
||||
const curBottom = r.top + r.height;
|
||||
setMinimized(true);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const fullH = panel.getBoundingClientRect().height;
|
||||
const curTop = parseInt(panel.style.top) || 0;
|
||||
const newTop = Math.max(getTopBound(), curTop - (fullH - 42));
|
||||
let newTop;
|
||||
if (anchorTop) {
|
||||
// Anchor to top: keep current top
|
||||
newTop = Math.max(topBound, curTop);
|
||||
} else {
|
||||
// Anchor to bottom: keep current bottom (existing behavior)
|
||||
newTop = Math.min(bottomBound - 42, Math.max(topBound, curBottom - 42));
|
||||
}
|
||||
panel.style.top = newTop + 'px';
|
||||
localStorage.setItem('f0ck_chat_float_y', newTop);
|
||||
});
|
||||
} else {
|
||||
// Expanding: use saved anchor or default to bottom if not set
|
||||
const wasAnchorTop = localStorage.getItem('f0ck_chat_anchor_top') === '1';
|
||||
setMinimized(false);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const fullH = panel.getBoundingClientRect().height;
|
||||
const curTop = parseInt(panel.style.top) || 0;
|
||||
let newTop;
|
||||
if (wasAnchorTop) {
|
||||
// Expanding from top anchor: top stays same
|
||||
newTop = Math.max(topBound, curTop);
|
||||
} else {
|
||||
// Expanding from bottom anchor: shift top UP (existing behavior)
|
||||
newTop = Math.max(topBound, curTop - (fullH - 42));
|
||||
}
|
||||
panel.style.top = newTop + 'px';
|
||||
localStorage.setItem('f0ck_chat_float_y', newTop);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setMinimized(!isMinimized);
|
||||
if (!isMinimized) {
|
||||
// Normal docked expand
|
||||
loadHistory();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1341,10 +1389,10 @@
|
||||
});
|
||||
|
||||
// ── Online users bar ─────────────────────────────────────────────────
|
||||
function renderOnline(users) {
|
||||
function renderOnline(users, guestCount = 0) {
|
||||
const el = document.getElementById('gchat-online');
|
||||
if (!el) return;
|
||||
if (!users || users.length === 0) {
|
||||
if ((!users || users.length === 0) && guestCount === 0) {
|
||||
el.innerHTML = '';
|
||||
el.style.display = 'none';
|
||||
return;
|
||||
@@ -1361,11 +1409,16 @@
|
||||
return `<img src="${src}" class="gchat-online-avatar" title="${name}" alt="${name}" loading="lazy" ${colorStyle}>`;
|
||||
}).join('');
|
||||
const extraPill = extra > 0 ? `<span class="gchat-online-extra">+${extra}</span>` : '';
|
||||
const countLabel = `<span class="gchat-online-count">${users.length} online</span>`;
|
||||
|
||||
let countText = `${users.length} online`;
|
||||
if (guestCount > 0 && (window.f0ckSession.is_admin || window.f0ckSession.is_moderator)) {
|
||||
countText += ` (${guestCount} guests)`;
|
||||
}
|
||||
const countLabel = `<span class="gchat-online-count">${countText}</span>`;
|
||||
el.innerHTML = `<div class="gchat-online-inner">${countLabel}<div class="gchat-online-avatars">${avatarHTML}${extraPill}</div></div>`;
|
||||
}
|
||||
document.addEventListener('f0ck:global_chat_presence', (e) => {
|
||||
renderOnline(e.detail?.users || []);
|
||||
renderOnline(e.detail?.users || [], e.detail?.guestCount || 0);
|
||||
});
|
||||
|
||||
// Event delegation: reply + admin delete buttons inside #gchat-messages
|
||||
|
||||
@@ -328,6 +328,13 @@ if (window.__dmLoaded) {
|
||||
currentOtherId = parseInt(thread.dataset.otherId, 10);
|
||||
myId = parseInt(thread.dataset.myId, 10);
|
||||
|
||||
// Reset state for a fresh load (essential if key just changed or multiple inits ran)
|
||||
renderedIds.clear();
|
||||
threadMessages = [];
|
||||
latestMsgId = 0;
|
||||
oldestMsgId = null;
|
||||
threadHasMore = false;
|
||||
|
||||
// Update page title to reflect the conversation
|
||||
const otherName = thread.dataset.otherName || '';
|
||||
document.title = otherName ? `DM with ${otherName}` : 'Messages';
|
||||
@@ -601,7 +608,7 @@ if (window.__dmLoaded) {
|
||||
// 6. Line-by-line rendering to avoid paragraph collapsing and recursion
|
||||
const renderedLines = escaped.split('\n').map(line => {
|
||||
const trimmed = line.trimStart();
|
||||
if (trimmed.startsWith('>')) {
|
||||
if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) {
|
||||
const quoteContent = line.substring(line.indexOf('>') + 1);
|
||||
return `<span class="greentext">>${quoteContent}</span>`;
|
||||
}
|
||||
@@ -659,6 +666,13 @@ if (window.__dmLoaded) {
|
||||
html = html.replace(ytEmbedRegex, (match, videoId) => {
|
||||
return `<span class="yt-embed-wrap"><iframe src="https://www.youtube.com/embed/${videoId}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen referrerpolicy="strict-origin-when-cross-origin"></iframe></span>`;
|
||||
});
|
||||
|
||||
// 7.5 Vocaroo embed logic
|
||||
const vocarooEmbedRegex = /(?:<p>)?\s*<a\s+[^>]*href="https?:\/\/(?:www\.)?(?:voca\.ro|vocaroo\.com)\/([a-zA-Z0-9_-]+)[^"]*"[^>]*>([\s\S]*?)<\/a>\s*(?:<\/p>)?/gi;
|
||||
html = html.replace(vocarooEmbedRegex, (match, vocarooId) => {
|
||||
if (['upload', 'contact', 'privacy', 'tos', 'about'].includes(vocarooId.toLowerCase())) return match;
|
||||
return `<span class="vocaroo-embed-wrap"><iframe src="https://vocaroo.com/embed/${vocarooId}?autoplay=0" width="300" height="60" frameborder="0" allow="autoplay"></iframe></span>`;
|
||||
});
|
||||
|
||||
// 8. Same-site video embed logic
|
||||
const escapedSiteHost = window.location.host.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
@@ -1298,6 +1312,8 @@ if (window.__dmLoaded) {
|
||||
localStorage.removeItem(DM_KEY_VERSION);
|
||||
_privateKey = null; _publicKeyJwk = null;
|
||||
await showRecoveryModal();
|
||||
// REFRESH UI after recovery
|
||||
if (typeof initMessagesPage === 'function') await initMessagesPage();
|
||||
resolve();
|
||||
};
|
||||
|
||||
@@ -1476,6 +1492,10 @@ if (window.__dmLoaded) {
|
||||
modal.querySelector('#dm-recover-phrase-btn').onclick = async () => {
|
||||
modal.style.display = 'none';
|
||||
await showRecoveryModal();
|
||||
|
||||
// REFRESH UI after recovery
|
||||
if (typeof initMessagesPage === 'function') await initMessagesPage();
|
||||
|
||||
// Refresh status label on re-open
|
||||
const statusEl = modal.querySelector('#dm-key-status');
|
||||
if (statusEl) statusEl.textContent = hasKey() ? '✅ Key loaded and backed up.' : '❌ No key found.';
|
||||
@@ -1494,6 +1514,8 @@ if (window.__dmLoaded) {
|
||||
const status = await loadOrCreateKeyPair();
|
||||
uploadPublicKey().catch(() => {});
|
||||
if (status === 'new') await showSeedSetupModal();
|
||||
// REFRESH UI after regen/setup
|
||||
if (typeof initMessagesPage === 'function') await initMessagesPage();
|
||||
} catch (e) {
|
||||
setMsg(msg, '❌ Error: ' + e.message, 'err');
|
||||
}
|
||||
@@ -1595,7 +1617,7 @@ if (window.__dmLoaded) {
|
||||
|
||||
const thread = document.getElementById('dm-thread');
|
||||
if (thread && currentOtherId && parseInt(thread.dataset.otherId) === currentOtherId) {
|
||||
console.log('[DM] Tab active: catching up on conversation...');
|
||||
window.f0ckDebug('[DM] Tab active: catching up on conversation...');
|
||||
appendNewMessages(thread).then(() => {
|
||||
dmFetch('POST', `/api/dm/read/${currentOtherId}`)
|
||||
.then(() => refreshDmBadge()) // refreshDmBadge clears title via updateDmBadge(0)
|
||||
@@ -1644,7 +1666,7 @@ if (window.__dmLoaded) {
|
||||
const thread = document.getElementById('dm-thread');
|
||||
if (!thread || !threadMessages.length) return;
|
||||
|
||||
console.log('[DM] Emojis ready, re-rendering thread...');
|
||||
window.f0ckDebug('[DM] Emojis ready, re-rendering thread...');
|
||||
const isAtBottom = (thread.scrollHeight - thread.scrollTop - thread.clientHeight) < 50;
|
||||
|
||||
// Clear and re-render from cache
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
* Protects against XSS by stripping disallowed tags and attributes.
|
||||
*/
|
||||
class Sanitizer {
|
||||
static ALLOWED_TAGS = ['span', 'img', 'a', 'br', 'b', 'i', 'strong', 'em', 'blockquote', 'pre', 'code', 'div', 'p', 'hr', 'ul', 'ol', 'li', 'textarea', 'button', 'input', 'label', 'select', 'option', 'svg', 'polyline', 'path', 'line', 'rect', 'circle', 'g', 'defs', 'symbol', 'use', 'polygon', 'ellipse', 'lineargradient', 'radialgradient', 'stop', 'clippath', 'mask', 'iframe', 'video', 'audio'];
|
||||
// F-009 Security: Removed form elements (textarea, button, input, label, select, option)
|
||||
// to prevent phishing via user-generated content (comments, DMs, chat).
|
||||
// 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_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:'];
|
||||
|
||||
@@ -56,9 +59,11 @@ class Sanitizer {
|
||||
if (this.DISALLOWED_URL_SCHEMES.some(scheme => val.startsWith(scheme))) {
|
||||
node.removeAttribute(attr.name);
|
||||
}
|
||||
// Iframes: only allow YouTube embed URLs
|
||||
// Iframes: allow YouTube and Vocaroo embed URLs
|
||||
if (attrName === 'src' && tagName === 'iframe') {
|
||||
if (!val.startsWith('https://www.youtube.com/embed/')) {
|
||||
const isYouTube = val.startsWith('https://www.youtube.com/embed/');
|
||||
const isVocaroo = val.startsWith('https://vocaroo.com/embed/');
|
||||
if (!isYouTube && !isVocaroo) {
|
||||
node.removeAttribute(attr.name);
|
||||
}
|
||||
}
|
||||
@@ -71,7 +76,14 @@ class Sanitizer {
|
||||
const styleParts = attr.value.split(';').filter(p => p.trim().length > 0);
|
||||
const cleanStyles = styleParts.filter(part => {
|
||||
const prop = part.split(':')[0].trim().toLowerCase();
|
||||
return safeStyles.includes(prop);
|
||||
if (!safeStyles.includes(prop)) return false;
|
||||
// F-009 Security: Strip url() from background/background-image
|
||||
// to prevent CSS-based tracking (e.g. background-image: url(https://evil.com/track))
|
||||
const val = part.split(':').slice(1).join(':').trim().toLowerCase();
|
||||
if ((prop === 'background-image' || prop === 'background') && /url\s*\(/i.test(val)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (cleanStyles.length > 0) {
|
||||
node.setAttribute(attr.name, cleanStyles.join('; '));
|
||||
|
||||
@@ -335,6 +335,7 @@
|
||||
|
||||
// Close popup if click outside
|
||||
document.addEventListener('click', e => {
|
||||
if (!document.body.classList.contains('scroller-active')) return;
|
||||
if (!volumePopup.contains(e.target) && e.target !== muteBtn && !muteBtn.contains(e.target)) {
|
||||
volumePopup.classList.remove('open');
|
||||
}
|
||||
@@ -369,6 +370,11 @@
|
||||
closePanel(filterPanel, filterBackdrop);
|
||||
closePanel(commentsPanel, commentsBackdrop);
|
||||
closePanel(settingsPanel, settingsBackdrop);
|
||||
if (typeof closeTagBar === 'function') closeTagBar();
|
||||
if (typeof closeSharePanel === 'function') closeSharePanel();
|
||||
if (typeof closeChanPanel === 'function') closeChanPanel();
|
||||
const active = document.activeElement;
|
||||
if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')) active.blur();
|
||||
}
|
||||
function addSwipeClose(panel, backdrop) {
|
||||
let startY = 0;
|
||||
@@ -690,10 +696,22 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Spoiler/blur reveal — delegated click on the comments list
|
||||
// Spoiler/blur reveal & context links — delegated click on the comments list
|
||||
commentsList.addEventListener('click', e => {
|
||||
const sp = e.target.closest('.scroller-spoiler, .scroller-blur');
|
||||
if (sp) sp.classList.toggle('revealed');
|
||||
|
||||
const contextLink = e.target.closest('.comment-context-link');
|
||||
if (contextLink) {
|
||||
e.preventDefault();
|
||||
const id = contextLink.dataset.id;
|
||||
const target = commentsList.querySelector(`.comment-item[data-comment-id="${id}"]`);
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
target.classList.add('highlight-comment');
|
||||
setTimeout(() => target.classList.remove('highlight-comment'), 2000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Slide activation ──────────────────────────────────────────────────────
|
||||
@@ -1593,6 +1611,7 @@
|
||||
is_video: isVideo,
|
||||
is_image: isImage,
|
||||
is_audio: false,
|
||||
comment_count: p.replies || 0,
|
||||
rating_label: isWsg ? 'SFW' : (isGif ? 'NSFW' : 'External'),
|
||||
rating_class: isWsg ? 'sfw' : (isGif ? 'nsfw' : 'untagged')
|
||||
};
|
||||
@@ -1618,6 +1637,7 @@
|
||||
item.username = m.username;
|
||||
item.display_name = m.display_name;
|
||||
item.avatar = m.avatar;
|
||||
if (m.comment_count != null) item.comment_count = m.comment_count;
|
||||
if (m.rating_class) { item.rating_class = m.rating_class; item.rating_label = m.rating_label; }
|
||||
|
||||
}
|
||||
@@ -1931,10 +1951,38 @@
|
||||
indicator.style.display = 'flex';
|
||||
}
|
||||
if (commentInput) {
|
||||
const quote = `>>${commentId} `;
|
||||
const start = commentInput.selectionStart;
|
||||
const end = commentInput.selectionEnd;
|
||||
const val = commentInput.value;
|
||||
commentInput.value = val.substring(0, start) + quote + val.substring(end);
|
||||
commentInput.focus();
|
||||
commentInput.selectionStart = commentInput.selectionEnd = start + quote.length;
|
||||
commentInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
|
||||
function quoteComment(id, username) {
|
||||
if (!commentInput) return;
|
||||
const commentEl = commentsList.querySelector(`.comment-item[data-comment-id="${id}"]`);
|
||||
if (!commentEl) return;
|
||||
const contentEl = commentEl.querySelector('.comment-content');
|
||||
if (!contentEl) return;
|
||||
|
||||
const raw = (contentEl.dataset.raw || '').replace(/<br\s*\/?>/gi, '\n').trim();
|
||||
const lines = raw.split('\n');
|
||||
const quote = `>>${id}\n${lines.map(line => `>${line}`).join('\n')}\n`;
|
||||
|
||||
const start = commentInput.selectionStart;
|
||||
const end = commentInput.selectionEnd;
|
||||
const val = commentInput.value;
|
||||
|
||||
commentInput.value = val.substring(0, start) + quote + val.substring(end);
|
||||
commentInput.focus();
|
||||
commentInput.selectionStart = commentInput.selectionEnd = start + quote.length;
|
||||
commentInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
function clearReply() {
|
||||
replyToCommentId = null;
|
||||
replyToUsername = null;
|
||||
@@ -1956,19 +2004,28 @@
|
||||
<img class="comment-avatar" src="${esc(av)}" alt="" loading="lazy" onerror="this.src='/a/default.png'">
|
||||
<div class="comment-body">
|
||||
<div class="comment-username" style="${nc}">${esc(uname)}</div>
|
||||
<div class="comment-content">${renderCommentContent(c.content || '')}</div>
|
||||
<div class="comment-content" data-raw="${esc(c.content || '')}">${renderCommentContent(c.content || '')}</div>
|
||||
<div class="comment-meta">
|
||||
<span class="comment-time">${c.created_at ? timeAgo(c.created_at) : (_i.ta_just_now || _i.just_now || 'just now')}</span>
|
||||
${canReply ? `<button class="comment-reply-btn" data-id="${c.id}" data-user="${esc(c.username || uname)}">${_i.reply || 'Reply'}</button>` : ''}
|
||||
${canReply ? `
|
||||
<button class="comment-reply-btn" data-id="${c.id}" data-user="${esc(c.username || uname)}">${_i.reply || 'Reply'}</button>
|
||||
<button class="comment-quote-btn" data-id="${c.id}" data-user="${esc(c.username || uname)}">${_i.quote || 'Quote'}</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
// Wire reply button
|
||||
// Wire buttons
|
||||
const replyBtn = el.querySelector('.comment-reply-btn');
|
||||
if (replyBtn) {
|
||||
replyBtn.addEventListener('click', () => {
|
||||
setReplyTo(replyBtn.dataset.id, replyBtn.dataset.user);
|
||||
});
|
||||
}
|
||||
const quoteBtn = el.querySelector('.comment-quote-btn');
|
||||
if (quoteBtn) {
|
||||
quoteBtn.addEventListener('click', () => {
|
||||
quoteComment(quoteBtn.dataset.id, quoteBtn.dataset.user);
|
||||
});
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
@@ -2009,6 +2066,7 @@
|
||||
const tagBarClose = document.getElementById('tag-bar-close-btn');
|
||||
|
||||
function openTagBar(itemId) {
|
||||
closeAllPanels();
|
||||
tagBarItemId = itemId;
|
||||
if (!tagBar) return;
|
||||
tagBar.classList.add('open');
|
||||
@@ -2022,11 +2080,9 @@
|
||||
closeSugg();
|
||||
tagBarItemId = null;
|
||||
const inp = document.getElementById('scroll-tag-input');
|
||||
if (inp) inp.value = '';
|
||||
if (inp) { inp.value = ''; inp.blur(); }
|
||||
}
|
||||
if (tagBarClose) tagBarClose.addEventListener('click', closeTagBar);
|
||||
// Close on Escape key
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape' && tagBar?.classList.contains('open')) closeTagBar(); });
|
||||
|
||||
// ── Share panel ────────────────────────────────────────────────────────────
|
||||
const sharePanel = document.getElementById('share-panel');
|
||||
@@ -2263,11 +2319,13 @@
|
||||
);
|
||||
|
||||
// Escape HTML first, then process line by line
|
||||
const escaped = esc(text);
|
||||
const normalized = text.replace(/<br\s*\/?>/gi, '\n');
|
||||
const escaped = esc(normalized);
|
||||
const lines = escaped.split('\n').map(line => {
|
||||
const trimmed = line.trimStart();
|
||||
// Greentext / quote: lines starting with >
|
||||
if (trimmed.startsWith('>')) {
|
||||
// Exclude only the numeric context links (>>ID) so they can be handled as interactive links.
|
||||
if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) {
|
||||
const after = line.substring(line.indexOf('>') + 4);
|
||||
const withEmoji = after.replace(/:([a-z0-9_]+):/g, (m, name) => {
|
||||
const url = customEmojis[name];
|
||||
@@ -2289,9 +2347,19 @@
|
||||
const url = customEmojis[name];
|
||||
return url ? `<img src="${esc(url)}" alt=":${esc(name)}:" title=":${esc(name)}:" style="height:1.8em;vertical-align:middle;display:inline-block;margin:0 2px">` : m;
|
||||
});
|
||||
|
||||
// 3. Replace >>ID patterns with context links
|
||||
out = out.replace(/(?<!\w)>>(\d+)/g, (match, id) => {
|
||||
return `<a href="#c${id}" class="comment-context-link" data-id="${id}">>>${id}</a>`;
|
||||
});
|
||||
|
||||
return out;
|
||||
});
|
||||
let html = lines.join('<br>');
|
||||
let html = lines.map((line, i) => {
|
||||
if (i === lines.length - 1) return line;
|
||||
if (line.includes('scroller-greentext') || line.includes('display:block')) return line;
|
||||
return line + '<br>';
|
||||
}).join('');
|
||||
// [spoiler]…[/spoiler] — iterative to handle nesting
|
||||
const spoilerRe = /\[spoiler\]((?:(?!\[spoiler\])[\s\S])*?)\[\/spoiler\]/gi;
|
||||
let prev, n = 0;
|
||||
@@ -2783,12 +2851,12 @@
|
||||
chanHashPending = fetch(`/api/v2/scroller/external/4chan/${board}/find/${postno}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
console.log('[CHAN] Find result:', data);
|
||||
window.f0ckDebug('[CHAN] Find result:', data);
|
||||
if (data.success && data.tid) {
|
||||
applied.externalUrl = `https://boards.4chan.org/${board}/thread/${data.tid}`;
|
||||
applied.order = 'oldest';
|
||||
unlock4chan();
|
||||
console.log('[CHAN] Set externalUrl:', applied.externalUrl);
|
||||
window.f0ckDebug('[CHAN] Set externalUrl:', applied.externalUrl);
|
||||
}
|
||||
})
|
||||
.catch(err => { console.error('[CHAN] Find error:', err); });
|
||||
@@ -2887,11 +2955,13 @@
|
||||
|
||||
// ── Keyboard ──────────────────────────────────────────────────────────────
|
||||
document.addEventListener('keydown', e => {
|
||||
if (!document.body.classList.contains('scroller-active')) return;
|
||||
const active = document.activeElement;
|
||||
const isTyping = active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable);
|
||||
|
||||
// 'c' toggles comments — only when NOT typing (so it never fires inside the comment input)
|
||||
if ((e.key === 'c' || e.key === 'C') && !isTyping && !tagBar?.classList.contains('open')) {
|
||||
e.preventDefault();
|
||||
if (commentsPanel.classList.contains('open')) closeAllPanels();
|
||||
else if (currentSlide) openComments(currentSlide.dataset.localId || currentSlide.dataset.id);
|
||||
return;
|
||||
@@ -2915,10 +2985,10 @@
|
||||
const idx = currentSlide ? slides.indexOf(currentSlide) : 0;
|
||||
if (e.key === 'ArrowDown' || e.key === 'j') { e.preventDefault(); const n = slides[idx + 1]; if (n) n.scrollIntoView({ behavior: 'smooth' }); }
|
||||
else if (e.key === 'ArrowUp' || e.key === 'k') { e.preventDefault(); const p = slides[idx - 1]; if (p) p.scrollIntoView({ behavior: 'smooth' }); }
|
||||
else if (e.key === 'm') { isMuted = !isMuted; if (!isMuted && volume === 0) volume = 0.5; syncVolumeUI(); applyVolumeToAll(); saveVolume(); prefs.startUnmuted = !isMuted; savePrefs(prefs); applyStartUnmuted(!isMuted); showVolumePopup(); }
|
||||
else if (e.key === 'm') { e.preventDefault(); isMuted = !isMuted; if (!isMuted && volume === 0) volume = 0.5; syncVolumeUI(); applyVolumeToAll(); saveVolume(); prefs.startUnmuted = !isMuted; savePrefs(prefs); applyStartUnmuted(!isMuted); showVolumePopup(); }
|
||||
else if (e.key === ' ') { e.preventDefault(); if (currentMedia) currentMedia.paused ? currentMedia.play().catch(() => {}) : currentMedia.pause(); }
|
||||
else if (e.key === 'f' || e.key === 'F') { pending = { ...applied, tags: [...applied.tags] }; syncPanelUI(); renderPresets(); openPanel(filterPanel, filterBackdrop); }
|
||||
else if (e.key === 'g' || e.key === 'G') { if (applied.externalUrl && chanGalleryBtn) toggleGallery(); }
|
||||
else if (e.key === 'f' || e.key === 'F') { e.preventDefault(); pending = { ...applied, tags: [...applied.tags] }; syncPanelUI(); renderPresets(); openPanel(filterPanel, filterBackdrop); }
|
||||
else if (e.key === 'g' || e.key === 'G') { e.preventDefault(); if (applied.externalUrl && chanGalleryBtn) toggleGallery(); }
|
||||
else if (e.key === 'r' || e.key === 'R') {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation(); // prevent f0ckm.js global 'r' random shortcut from firing
|
||||
@@ -2982,7 +3052,7 @@
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
else if (e.key === 'l' || e.key === 'L') { if (currentSlide) toggleFav(currentSlide); }
|
||||
else if (e.key === 'l' || e.key === 'L') { e.preventDefault(); if (currentSlide) toggleFav(currentSlide); }
|
||||
else if (e.key === 'i' || e.key === 'I') { e.preventDefault(); if (currentSlide) openTagBar(currentSlide.dataset.id); }
|
||||
else if (e.key === 'e' || e.key === 'E') { e.preventDefault(); e.stopImmediatePropagation(); } // suppress upload modal shortcut in abyss
|
||||
else if (e.key === 'Escape') { window.location.href = '/'; }
|
||||
@@ -3000,6 +3070,7 @@
|
||||
let fourCount = 0;
|
||||
let fourTimer = null;
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!document.body.classList.contains('scroller-active')) return;
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||
if (e.key === '4') {
|
||||
fourCount++;
|
||||
@@ -3407,6 +3478,7 @@
|
||||
|
||||
// Close on outside click
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!document.body.classList.contains('scroller-active')) return;
|
||||
if (sNotifOpen && !sNotifDropdown.contains(e.target) && !sNotifBtn.contains(e.target)) {
|
||||
sNotifOpen = false;
|
||||
sNotifDropdown.classList.remove('visible');
|
||||
|
||||
@@ -612,6 +612,97 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Comment Display Mode Toggle
|
||||
const commentDisplayModeSelect = document.getElementById('comment_display_mode_select');
|
||||
if (commentDisplayModeSelect) {
|
||||
commentDisplayModeSelect.addEventListener('change', async () => {
|
||||
const mode = parseInt(commentDisplayModeSelect.value, 10);
|
||||
try {
|
||||
const res = await fetch('/api/v2/settings/comment_display_mode', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.f0ckSession?.csrf_token
|
||||
},
|
||||
body: JSON.stringify({ mode })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
showStatus('Comment display mode updated!', 'success');
|
||||
if (window.f0ckSession) window.f0ckSession.comment_display_mode = mode;
|
||||
} else {
|
||||
alert(data.msg || 'Error saving preference');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Failed to save preference');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Alternative Infobox Toggle (legacy layout only)
|
||||
const alternativeInfoboxToggle = document.getElementById('alternative_infobox_toggle');
|
||||
if (alternativeInfoboxToggle) {
|
||||
alternativeInfoboxToggle.addEventListener('change', async () => {
|
||||
const use_alternative_infobox = alternativeInfoboxToggle.checked;
|
||||
try {
|
||||
const res = await fetch('/api/v2/settings/alternative_infobox', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': window.f0ckSession?.csrf_token
|
||||
},
|
||||
body: new URLSearchParams({ use_alternative_infobox })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
showStatus('Infobox preference updated!', 'success');
|
||||
if (window.f0ckSession) window.f0ckSession.use_alternative_infobox = use_alternative_infobox;
|
||||
} else {
|
||||
alert(data.msg || 'Error saving preference');
|
||||
alternativeInfoboxToggle.checked = !use_alternative_infobox;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Failed to save infobox preference');
|
||||
alternativeInfoboxToggle.checked = !use_alternative_infobox;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Notification Preferences Toggles
|
||||
const setupPreferenceToggle = (id, sessionKey) => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.addEventListener('change', async () => {
|
||||
const enabled = el.checked;
|
||||
try {
|
||||
const res = await fetch('/api/v2/settings/notifications', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': window.f0ckSession?.csrf_token
|
||||
},
|
||||
body: new URLSearchParams({ key: sessionKey, value: enabled })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
if (window.f0ckSession) window.f0ckSession[sessionKey] = enabled;
|
||||
} else {
|
||||
alert(data.msg || 'Error saving preference');
|
||||
el.checked = !enabled;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
el.checked = !enabled;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
setupPreferenceToggle('chk-receive-system-notifications', 'receive_system_notifications');
|
||||
setupPreferenceToggle('chk-receive-user-notifications', 'receive_user_notifications');
|
||||
setupPreferenceToggle('chk-do-not-disturb', 'do_not_disturb');
|
||||
|
||||
const wheelToggle = document.getElementById('wheel_nav_toggle');
|
||||
if (wheelToggle) {
|
||||
wheelToggle.checked = localStorage.getItem('wheelNavEnabled') === 'true';
|
||||
@@ -1255,26 +1346,16 @@
|
||||
}
|
||||
|
||||
// ==== Ruffle (Flash) Settings ====
|
||||
const ruffleVolumeInput = document.getElementById('ruffle_volume_input');
|
||||
const ruffleVolumeVal = document.getElementById('ruffle_volume_val');
|
||||
const ruffleBackToggle = document.getElementById('ruffle_background_toggle');
|
||||
const ruffleSaveBtn = document.getElementById('btn-save-ruffle-settings');
|
||||
const ruffleStatus = document.getElementById('ruffle-settings-status');
|
||||
|
||||
if (ruffleVolumeInput && ruffleVolumeVal) {
|
||||
ruffleVolumeInput.addEventListener('input', () => {
|
||||
ruffleVolumeVal.textContent = Math.round(ruffleVolumeInput.value * 100) + '%';
|
||||
});
|
||||
}
|
||||
|
||||
if (ruffleSaveBtn) {
|
||||
ruffleSaveBtn.addEventListener('click', async () => {
|
||||
const ruffle_volume = parseFloat(ruffleVolumeInput.value);
|
||||
|
||||
if (ruffleBackToggle) {
|
||||
ruffleBackToggle.addEventListener('change', async () => {
|
||||
const ruffle_background = ruffleBackToggle.checked;
|
||||
|
||||
ruffleSaveBtn.disabled = true;
|
||||
ruffleSaveBtn.textContent = i18n.saving || 'Saving...';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/v2/settings/ruffle', {
|
||||
method: 'PUT',
|
||||
@@ -1282,13 +1363,12 @@
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.f0ckSession?.csrf_token
|
||||
},
|
||||
body: JSON.stringify({ ruffle_volume, ruffle_background })
|
||||
body: JSON.stringify({ ruffle_background })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
showAccountStatus(ruffleStatus, 'Flash settings updated correctly!', 'success');
|
||||
showAccountStatus(ruffleStatus, 'Flash settings updated!', 'success');
|
||||
if (window.f0ckSession) {
|
||||
window.f0ckSession.ruffle_volume = ruffle_volume;
|
||||
window.f0ckSession.ruffle_background = ruffle_background;
|
||||
|
||||
// Apply to the active Ruffle player if it exists so user doesn't need to refresh
|
||||
@@ -1296,7 +1376,6 @@
|
||||
if (ruffleContainer) {
|
||||
const player = ruffleContainer.querySelector('ruffle-player') || ruffleContainer.querySelector('ruffle-object');
|
||||
if (player) {
|
||||
player.volume = ruffle_volume;
|
||||
// Ruffle doesn't dynamically toggle pageVisibility well without recreation,
|
||||
// but we can update the config for subsequent initializations
|
||||
if (window.RufflePlayer && window.RufflePlayer.config) {
|
||||
@@ -1308,13 +1387,12 @@
|
||||
}
|
||||
} else {
|
||||
showAccountStatus(ruffleStatus, data.msg || 'Failed to update Flash settings', 'error');
|
||||
ruffleBackToggle.checked = !ruffle_background; // Revert on failure
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showAccountStatus(ruffleStatus, 'Request failed', 'error');
|
||||
} finally {
|
||||
ruffleSaveBtn.disabled = false;
|
||||
ruffleSaveBtn.textContent = 'Save Flash Settings';
|
||||
ruffleBackToggle.checked = !ruffle_background; // Revert on error
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,7 +37,56 @@
|
||||
return div.innerHTML;
|
||||
};
|
||||
|
||||
const renderCommentContent = (content) => {
|
||||
const ytOembedCache = new Map(); // videoId -> meta object
|
||||
const ytOembedPending = new Map(); // videoId -> Promise
|
||||
|
||||
const fetchSidebarYoutubeTitles = async (container) => {
|
||||
const links = container.querySelectorAll('.sidebar-video-link[data-yt-id]');
|
||||
if (links.length === 0) return;
|
||||
|
||||
for (const link of links) {
|
||||
const videoId = link.dataset.ytId;
|
||||
if (!videoId) continue;
|
||||
|
||||
const titleSpan = link.querySelector('.yt-title');
|
||||
if (!titleSpan || titleSpan.dataset.loaded === 'true') continue;
|
||||
|
||||
let meta = ytOembedCache.get(videoId);
|
||||
if (!meta) {
|
||||
if (ytOembedPending.has(videoId)) {
|
||||
meta = await ytOembedPending.get(videoId);
|
||||
} else {
|
||||
const promise = (async () => {
|
||||
try {
|
||||
const ytUrl = `https://www.youtube.com/watch?v=${encodeURIComponent(videoId)}`;
|
||||
const r = await fetch(`/api/v2/meta/fetch?url=${encodeURIComponent(ytUrl)}`);
|
||||
if (r.ok) {
|
||||
const data = await r.json();
|
||||
if (data.success && data.meta) {
|
||||
ytOembedCache.set(videoId, data.meta);
|
||||
return data.meta;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return null;
|
||||
})();
|
||||
ytOembedPending.set(videoId, promise);
|
||||
meta = await promise;
|
||||
ytOembedPending.delete(videoId);
|
||||
}
|
||||
}
|
||||
|
||||
if (meta && meta.title) {
|
||||
titleSpan.textContent = meta.title;
|
||||
} else {
|
||||
// If title fails, just leave it blank or use a generic label
|
||||
titleSpan.textContent = 'YouTube Video';
|
||||
}
|
||||
titleSpan.dataset.loaded = 'true';
|
||||
}
|
||||
};
|
||||
|
||||
const renderCommentContent = (content, commentId = null, itemId = null) => {
|
||||
if (!content) return '';
|
||||
|
||||
// Anti-recursion / Performance safeguard for extremely long comments
|
||||
@@ -149,7 +198,7 @@
|
||||
// Line-by-line rendering to avoid paragraph collapsing and recursion
|
||||
const renderedLines = escaped.split('\n').map(line => {
|
||||
const trimmed = line.trimStart();
|
||||
if (trimmed.startsWith('>')) {
|
||||
if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) {
|
||||
// Manual greentext handling — apply emoji if the user preference allows it
|
||||
const quoteContent = line.substring(line.indexOf('>') + 1);
|
||||
const quoteEmojis = window.f0ckSession?.quote_emojis === true;
|
||||
@@ -166,10 +215,19 @@
|
||||
|
||||
// Perform replacements on the single line
|
||||
let processedLine = line;
|
||||
|
||||
// Handle Mentions
|
||||
processedLine = processedLine.replace(mentionRegex, (match, g1, g2) => {
|
||||
const user = g1 || g2;
|
||||
return `<a href="/user/${encodeURIComponent(user)}" class="mention">@${user}</a>`;
|
||||
});
|
||||
|
||||
// Handle Comment Context Links (>>ID)
|
||||
processedLine = processedLine.replace(/(?<!\w)>>(\d+)/g, (match, id) => {
|
||||
const targetHref = itemId ? `/${itemId}#c${id}` : `#c${id}`;
|
||||
return `<a href="${targetHref}" class="comment-context-link" data-id="${id}">>>${id}</a>`;
|
||||
});
|
||||
|
||||
processedLine = processedLine.replace(imageRegex, (match, url) => {
|
||||
let fullUrl = url;
|
||||
if (!url.startsWith('http') && !url.startsWith('//') && !url.startsWith('/')) {
|
||||
@@ -196,10 +254,25 @@
|
||||
// YouTube label replacement: show icon + labeled link
|
||||
md = md.replace(
|
||||
/<a\s[^>]*href="https?:\/\/(?:www\.)?(?:youtube\.com\/watch\?(?:[^"]*&(?:amp;)?)?v=|youtu\.be\/)([a-zA-Z0-9_\-]{11})[^"]*"[^>]*>([\s\S]*?)<\/a>/gi,
|
||||
(match) => {
|
||||
(match, videoId) => {
|
||||
const hrefMatch = match.match(/href="([^"]+)"/i);
|
||||
const href = hrefMatch ? hrefMatch[1] : '#';
|
||||
return `<a href="${href}" target="_blank" rel="noopener noreferrer" class="sidebar-video-link"><i class="fa-brands fa-youtube"></i></a>`;
|
||||
const ytHref = hrefMatch ? hrefMatch[1] : '#';
|
||||
const targetHref = (itemId && commentId) ? `/${itemId}#c${commentId}` : (commentId ? `#sc${commentId}` : ytHref);
|
||||
const externalAttr = (itemId && commentId) || commentId ? '' : ' target="_blank" rel="noopener noreferrer"';
|
||||
return `<a href="${targetHref}"${externalAttr} class="sidebar-video-link" data-yt-id="${videoId}"><i class="fa-brands fa-youtube"></i> <span class="yt-title"></span></a>`;
|
||||
}
|
||||
);
|
||||
|
||||
// Vocaroo label replacement
|
||||
md = md.replace(
|
||||
/<a\s[^>]*href="https?:\/\/(?:www\.)?(?:voca\.ro|vocaroo\.com)\/([a-zA-Z0-9_-]+)[^"]*"[^>]*>([\s\S]*?)<\/a>/gi,
|
||||
(match, vocarooId) => {
|
||||
if (['upload', 'contact', 'privacy', 'tos', 'about'].includes(vocarooId.toLowerCase())) return match;
|
||||
const hrefMatch = match.match(/href="([^"]+)"/i);
|
||||
const vocaHref = hrefMatch ? hrefMatch[1] : '#';
|
||||
const targetHref = (itemId && commentId) ? `/${itemId}#c${commentId}` : (commentId ? `#sc${commentId}` : vocaHref);
|
||||
const externalAttr = (itemId && commentId) || commentId ? '' : ' target="_blank" rel="noopener noreferrer"';
|
||||
return `<a href="${targetHref}"${externalAttr} class="sidebar-video-link"><i class="fa-solid fa-microphone"></i> <span>Vocaroo Audio</span></a>`;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -258,6 +331,10 @@
|
||||
return codeBlocks[index] || '';
|
||||
});
|
||||
|
||||
if (window.Sanitizer && typeof window.Sanitizer.clean === 'function') {
|
||||
md = window.Sanitizer.clean(md);
|
||||
}
|
||||
|
||||
return md;
|
||||
} catch (e) {
|
||||
return content;
|
||||
@@ -269,7 +346,7 @@
|
||||
|
||||
const renderActivityItem = (c) => {
|
||||
const rawContent = c.content || c.body || '';
|
||||
const displayContent = renderCommentContent(rawContent);
|
||||
const displayContent = renderCommentContent(rawContent, c.id, c.item_id);
|
||||
|
||||
// Build avatar URL — same priority as the rest of the app
|
||||
let avatarSrc = '/a/default.png';
|
||||
@@ -277,6 +354,7 @@
|
||||
avatarSrc = `/a/${c.avatar_file}`;
|
||||
} else if (c.avatar) {
|
||||
avatarSrc = `/t/${c.avatar}.webp`;
|
||||
if (window.applyThumbCacheBust) avatarSrc = window.applyThumbCacheBust(avatarSrc);
|
||||
}
|
||||
|
||||
const timeStr = c.created_at
|
||||
@@ -287,7 +365,9 @@
|
||||
let itemPreview = '';
|
||||
if (c.item_id) {
|
||||
let mediaHtml = '';
|
||||
mediaHtml = `<img src="/t/${c.item_id}.webp" style="width: 32px; height: 32px; object-fit: cover; border-radius: 2px;" onerror="this.style.display='none'" />`;
|
||||
let thumbUrl = `/t/${c.item_id}.webp`;
|
||||
if (window.applyThumbCacheBust) thumbUrl = window.applyThumbCacheBust(thumbUrl);
|
||||
mediaHtml = `<img src="${thumbUrl}" style="width: 32px; height: 32px; object-fit: cover; border-radius: 2px;" onerror="this.style.display='none'" />`;
|
||||
|
||||
itemPreview = `
|
||||
<div class="item-preview">
|
||||
@@ -308,7 +388,7 @@
|
||||
</div>
|
||||
<span class="comment-time timeago" style="font-size: 0.75em;"${tsAttr}>${timeStr}</span>
|
||||
</div>
|
||||
<div class="comment-content" style="font-size: 0.85em; line-height: 1.3;"><div class="comment-content-inner">${displayContent}</div><button class="read-more-btn">${window.f0ckI18n?.sidebar_read_more || 'read more'}</button></div>
|
||||
<div class="comment-content"><div class="comment-content-inner">${displayContent}</div><button class="read-more-btn">${window.f0ckI18n?.sidebar_read_more || 'read more'}</button></div>
|
||||
${itemPreview}
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -361,16 +441,13 @@
|
||||
html += renderActivityItem(c);
|
||||
});
|
||||
|
||||
if (window.Sanitizer) {
|
||||
container.innerHTML = window.Sanitizer.clean(html);
|
||||
} else {
|
||||
container.innerHTML = html;
|
||||
}
|
||||
container.innerHTML = html;
|
||||
// Re-append IO sentinel so the scroll observer keeps working after re-renders
|
||||
if (ioSentinel) {
|
||||
container.appendChild(ioSentinel);
|
||||
}
|
||||
checkOverflow();
|
||||
fetchSidebarYoutubeTitles(container);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -462,17 +539,14 @@
|
||||
newComments.forEach(c => { html += renderActivityItem(c); });
|
||||
if (html) {
|
||||
const temp = document.createElement('div');
|
||||
if (window.Sanitizer) {
|
||||
temp.innerHTML = window.Sanitizer.clean(html);
|
||||
} else {
|
||||
temp.innerHTML = html;
|
||||
}
|
||||
temp.innerHTML = html;
|
||||
while (temp.firstElementChild) {
|
||||
container.appendChild(temp.firstElementChild);
|
||||
}
|
||||
// Keep the IO sentinel at the very end so it triggers on the next scroll
|
||||
if (ioSentinel) container.appendChild(ioSentinel);
|
||||
checkOverflow();
|
||||
fetchSidebarYoutubeTitles(container);
|
||||
}
|
||||
} else {
|
||||
hasMore = false;
|
||||
@@ -496,7 +570,7 @@
|
||||
|
||||
// 1. Deduplicate: check if this comment ID is already in the cache
|
||||
if (window._sidebarActivityCache.some(c => parseInt(c.id) === parseInt(data.id))) {
|
||||
console.log("Sidebar Activity: Duplicate comment ignored", data.id);
|
||||
window.f0ckDebug("Sidebar Activity: Duplicate comment ignored", data.id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -512,16 +586,13 @@
|
||||
if (container) {
|
||||
const html = renderActivityItem(newItem);
|
||||
const temp = document.createElement('div');
|
||||
if (window.Sanitizer) {
|
||||
temp.innerHTML = window.Sanitizer.clean(html);
|
||||
} else {
|
||||
temp.innerHTML = html;
|
||||
}
|
||||
const node = temp.firstElementChild;
|
||||
if (node) {
|
||||
node.classList.add('new-item-fade');
|
||||
container.prepend(node);
|
||||
checkOverflow();
|
||||
fetchSidebarYoutubeTitles(container);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -533,7 +604,7 @@
|
||||
|
||||
// Listen for live activity from f0ckm.js
|
||||
document.addEventListener('f0ck:activityReceived', (e) => {
|
||||
console.log("Sidebar Activity: Live update received", e.detail);
|
||||
window.f0ckDebug("Sidebar Activity: Live update received", e.detail);
|
||||
handleNewActivity(e.detail);
|
||||
});
|
||||
|
||||
@@ -555,18 +626,20 @@
|
||||
if (el) {
|
||||
const inner = el.querySelector('.comment-content-inner');
|
||||
if (inner) {
|
||||
inner.innerHTML = renderCommentContent(data.content);
|
||||
const comment = window._sidebarActivityCache.find(c => String(c.id) === String(data.comment_id));
|
||||
inner.innerHTML = renderCommentContent(data.content, data.comment_id, comment ? comment.item_id : null);
|
||||
el.classList.remove('new-item-fade');
|
||||
void el.offsetWidth;
|
||||
el.classList.add('new-item-fade');
|
||||
checkOverflow();
|
||||
fetchSidebarYoutubeTitles(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('f0ck:comment_edited', (e) => {
|
||||
console.log("Sidebar Activity: Live edit received", e.detail);
|
||||
window.f0ckDebug("Sidebar Activity: Live edit received", e.detail);
|
||||
handleLiveEdit(e.detail);
|
||||
});
|
||||
|
||||
@@ -578,7 +651,7 @@
|
||||
const modeChanged = lastBoundMode !== null && lastBoundMode !== currentMode;
|
||||
lastBoundMode = currentMode;
|
||||
|
||||
console.log("Sidebar Activity: Page transition detected", modeChanged ? "(Mode changed)" : "");
|
||||
window.f0ckDebug("Sidebar Activity: Page transition detected", modeChanged ? "(Mode changed)" : "");
|
||||
|
||||
if (modeChanged) {
|
||||
window._sidebarActivityCache = [];
|
||||
@@ -600,7 +673,7 @@
|
||||
|
||||
// Handle explicit mode changes (e.g. from item page where full transition doesn't occur)
|
||||
document.addEventListener('f0ck:modeChanged', (e) => {
|
||||
console.log("Sidebar Activity: Mode change detected", e.detail.mode);
|
||||
window.f0ckDebug("Sidebar Activity: Mode change detected", e.detail.mode);
|
||||
lastBoundMode = e.detail.mode;
|
||||
window._sidebarActivityCache = [];
|
||||
currentPage = 1;
|
||||
@@ -610,7 +683,7 @@
|
||||
|
||||
// When the current user posts a comment, silently refresh sidebar to show it
|
||||
document.addEventListener('f0ck:commentPosted', () => {
|
||||
console.log("Sidebar Activity: Own comment posted, refreshing...");
|
||||
window.f0ckDebug("Sidebar Activity: Own comment posted, refreshing...");
|
||||
loadActivity(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -122,6 +122,17 @@ window.F0ckUpload = class {
|
||||
try {
|
||||
const res = JSON.parse(xhr.responseText);
|
||||
if (res.success) {
|
||||
if (res.itemid) {
|
||||
try {
|
||||
const ts = Date.now();
|
||||
const bustedStr = localStorage.getItem('bustedThumbs');
|
||||
const busted = bustedStr ? JSON.parse(bustedStr) : {};
|
||||
busted[res.itemid] = ts;
|
||||
const keys = Object.keys(busted);
|
||||
if (keys.length > 50) delete busted[keys[0]];
|
||||
localStorage.setItem('bustedThumbs', JSON.stringify(busted));
|
||||
} catch(e) {}
|
||||
}
|
||||
this.onComplete(res);
|
||||
resolve(res);
|
||||
} else {
|
||||
|
||||
@@ -40,6 +40,7 @@ window.initUploadForm = (selector) => {
|
||||
const dragModal = form.closest('#upload-drag-modal');
|
||||
if (dragModal) {
|
||||
dragModal.classList.remove('show');
|
||||
document.body.classList.remove('modal-open');
|
||||
if (form._f0ckUploader && typeof form._f0ckUploader.reset === 'function') {
|
||||
form._f0ckUploader.reset();
|
||||
}
|
||||
@@ -59,8 +60,7 @@ window.initUploadForm = (selector) => {
|
||||
const ytRegex = /(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\/?\?(?:\S*?&?v\=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11})/i;
|
||||
|
||||
// Dynamically get min tags requirement from DOM
|
||||
const minTagsRaw = tagCount?.textContent.match(/\/(\d+)/);
|
||||
const minTags = minTagsRaw ? parseInt(minTagsRaw[1]) : 3;
|
||||
const minTags = parseInt(form.getAttribute('data-min-tags') || '3');
|
||||
|
||||
let tags = [];
|
||||
let autoTags = []; // Track tags suggested from metadata
|
||||
@@ -465,8 +465,12 @@ window.initUploadForm = (selector) => {
|
||||
}
|
||||
|
||||
if (tagCount) {
|
||||
tagCount.textContent = '(' + tags.length + '/' + minTags + ' minimum)';
|
||||
tagCount.classList.toggle('valid', tags.length >= minTags);
|
||||
if (minTags > 0) {
|
||||
tagCount.textContent = '(' + tags.length + '/' + minTags + ' minimum)';
|
||||
tagCount.classList.toggle('valid', tags.length >= minTags);
|
||||
} else {
|
||||
tagCount.style.display = 'none';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -567,6 +571,10 @@ window.initUploadForm = (selector) => {
|
||||
previewElem = document.createElement('div');
|
||||
previewElem.className = 'generic-file-icon swf-preview-icon';
|
||||
previewElem.innerHTML = '<span style="font-size:2.5em;">⚡</span><br><span style="font-size:0.85em;letter-spacing:0.1em;color:#e040fb;font-weight:bold;">SWF</span>';
|
||||
} else if (file.type === 'application/pdf' || fileExt === 'pdf') {
|
||||
previewElem = document.createElement('div');
|
||||
previewElem.className = 'generic-file-icon pdf-preview-icon';
|
||||
previewElem.innerHTML = '<span style="font-size:2.5em;"><i class="fa-solid fa-file-pdf"></i></span><br><span style="font-size:0.85em;letter-spacing:0.1em;color:#ef5350;font-weight:bold;"></span>';
|
||||
} else {
|
||||
previewElem = document.createElement('div');
|
||||
previewElem.className = 'generic-file-icon';
|
||||
@@ -951,7 +959,7 @@ window.initUploadForm = (selector) => {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
console.log('[UPLOAD] Rendering ' + filtered.length + ' suggestions');
|
||||
window.f0ckDebug('[UPLOAD] Rendering ' + filtered.length + ' suggestions');
|
||||
if (tagSuggestions) {
|
||||
tagSuggestions.innerHTML = html;
|
||||
tagSuggestions.style.display = 'block';
|
||||
@@ -1087,11 +1095,26 @@ window.initUploadForm = (selector) => {
|
||||
}
|
||||
|
||||
if (dragModal) dragModal.classList.remove('show');
|
||||
if (window.resetGlobalScrollState) window.resetGlobalScrollState();
|
||||
if (window.hideAllModals) window.hideAllModals();
|
||||
form._f0ckUploader.reset();
|
||||
if (!dragModal) {
|
||||
statusDiv.innerHTML = '✓ ' + data.msg;
|
||||
statusDiv.className = 'upload-status success';
|
||||
}
|
||||
|
||||
if (data.itemid) {
|
||||
try {
|
||||
const ts = Date.now();
|
||||
const bustedStr = localStorage.getItem('bustedThumbs');
|
||||
const busted = bustedStr ? JSON.parse(bustedStr) : {};
|
||||
busted[data.itemid] = ts;
|
||||
const keys = Object.keys(busted);
|
||||
if (keys.length > 50) delete busted[keys[0]];
|
||||
localStorage.setItem('bustedThumbs', JSON.stringify(busted));
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
if (data.manual_approval && typeof window.showFlash === 'function') {
|
||||
window.showFlash('Upload awaits approval, please be patient', 'info');
|
||||
}
|
||||
@@ -1190,6 +1213,19 @@ window.initUploadForm = (selector) => {
|
||||
statusDiv.innerHTML = '✓ ' + res.msg;
|
||||
statusDiv.className = 'upload-status success';
|
||||
}
|
||||
|
||||
if (res.itemid) {
|
||||
try {
|
||||
const ts = Date.now();
|
||||
const bustedStr = localStorage.getItem('bustedThumbs');
|
||||
const busted = bustedStr ? JSON.parse(bustedStr) : {};
|
||||
busted[res.itemid] = ts;
|
||||
const keys = Object.keys(busted);
|
||||
if (keys.length > 50) delete busted[keys[0]];
|
||||
localStorage.setItem('bustedThumbs', JSON.stringify(busted));
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
if (res.manual_approval && typeof window.showFlash === 'function') {
|
||||
window.showFlash('Upload awaits approval', 'info');
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class UserCommentSystem {
|
||||
if (el && this.container.contains(el)) {
|
||||
const contentEl = el.querySelector('.comment-content');
|
||||
if (contentEl) {
|
||||
contentEl.innerHTML = this.renderCommentContent(data.content);
|
||||
contentEl.innerHTML = this.renderCommentContent(data.content, data.item_id);
|
||||
el.classList.remove('new-item-fade');
|
||||
void el.offsetWidth;
|
||||
el.classList.add('new-item-fade');
|
||||
@@ -77,7 +77,7 @@ class UserCommentSystem {
|
||||
// Check if this instance is still active
|
||||
if (!document.body.contains(this.container)) return;
|
||||
|
||||
console.log('Mode changed, reloading comments...');
|
||||
window.f0ckDebug('Mode changed, reloading comments...');
|
||||
this.container.innerHTML = '';
|
||||
this.page = 1;
|
||||
this.finished = false;
|
||||
@@ -108,7 +108,7 @@ class UserCommentSystem {
|
||||
this.userColor = json.user.username_color;
|
||||
}
|
||||
json.comments.forEach(c => {
|
||||
console.log('Raw Comment Content (ID ' + c.id + '):', c.content);
|
||||
window.f0ckDebug('Raw Comment Content (ID ' + c.id + '):', c.content);
|
||||
const html = this.renderComment(c);
|
||||
this.container.insertAdjacentHTML('beforeend', html);
|
||||
});
|
||||
@@ -134,7 +134,7 @@ class UserCommentSystem {
|
||||
return match;
|
||||
}
|
||||
|
||||
renderCommentContent(content) {
|
||||
renderCommentContent(content, itemId = null) {
|
||||
if (!content) return '';
|
||||
|
||||
// Anti-recursion / Performance safeguard for extremely long comments
|
||||
@@ -194,7 +194,7 @@ class UserCommentSystem {
|
||||
// 3. Line-by-line rendering to avoid paragraph collapsing and recursion
|
||||
const renderedLines = escaped.split('\n').map(line => {
|
||||
const trimmed = line.trimStart();
|
||||
if (trimmed.startsWith('>')) {
|
||||
if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) {
|
||||
const quoteContent = line.substring(line.indexOf('>') + 1);
|
||||
return `<span class="greentext">>${quoteContent}</span>`;
|
||||
}
|
||||
@@ -204,6 +204,13 @@ class UserCommentSystem {
|
||||
if (!line.trim()) return ' ';
|
||||
|
||||
let processedLine = line.replace(mentionRegex, '[@$1](/user/$1)');
|
||||
|
||||
// Handle Comment Context Links (>>ID)
|
||||
processedLine = processedLine.replace(/(?<!\w)>>(\d+)/g, (match, id) => {
|
||||
const targetHref = itemId ? `/${itemId}#c${id}` : `#c${id}`;
|
||||
return `<a href="${targetHref}" class="comment-context-link" data-id="${id}">>>${id}</a>`;
|
||||
});
|
||||
|
||||
const escapedAsterisks = processedLine.replace(/\*/g, '\\*');
|
||||
let rendered = marked.parseInline ? marked.parseInline(escapedAsterisks, { renderer: renderer }) : marked.parse(escapedAsterisks, { renderer: renderer }).replace(/<p>|<\/p>/g, '');
|
||||
|
||||
@@ -241,6 +248,10 @@ class UserCommentSystem {
|
||||
iterations++;
|
||||
} while (html !== prevMd && iterations < 10);
|
||||
|
||||
if (window.Sanitizer && typeof Sanitizer.clean === 'function') {
|
||||
html = Sanitizer.clean(html);
|
||||
}
|
||||
|
||||
return html;
|
||||
} catch (e) {
|
||||
console.error('UserCommentSystem Markdown Render Error:', e);
|
||||
@@ -251,7 +262,7 @@ class UserCommentSystem {
|
||||
renderComment(c) {
|
||||
const timeAgo = this.timeAgo(c.created_at);
|
||||
const fullDate = new Date(c.created_at).toISOString();
|
||||
const content = this.renderCommentContent(c.content);
|
||||
const content = this.renderCommentContent(c.content, c.item_id);
|
||||
|
||||
// Replicating the structure of comments.js but adapting for the list view
|
||||
// We add a header indicating which item this comment belongs to
|
||||
|
||||
@@ -96,7 +96,7 @@ class v0ck {
|
||||
if (["video", "audio"].includes(tagName)) {
|
||||
const parent = elem.parentElement;
|
||||
if (parent.querySelector('.v0ck_player_controls')) {
|
||||
console.log("[v0ck] Player controls already exist, skipping injection and init");
|
||||
window.f0ckDebug("[v0ck] Player controls already exist, skipping injection and init");
|
||||
return elem; // Return the video element as the constructor result
|
||||
} else {
|
||||
parent.classList.add("v0ck", "paused");
|
||||
@@ -123,7 +123,7 @@ class v0ck {
|
||||
// Use absolute path for reliable asset loading
|
||||
const size = elem.getAttribute('data-size');
|
||||
elem.insertAdjacentHTML("afterend", tpl_player(`/s/img/v0ck.svg`, size));
|
||||
console.log("[v0ck] Player initialized for", tagName);
|
||||
window.f0ckDebug("[v0ck] Player initialized for", tagName);
|
||||
}
|
||||
|
||||
if (tagName === "audio" && elem.hasAttribute('poster')) { // set cover
|
||||
@@ -430,8 +430,12 @@ class v0ck {
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize switch state (defaults to ON if not explicitly 'false')
|
||||
if (toggleBgSwitch && localStorage.getItem('background') !== 'false') {
|
||||
// Initialize switch state
|
||||
const bgEnabled = (window.f0ckSession && window.f0ckSession.show_background !== undefined)
|
||||
? !!window.f0ckSession.show_background
|
||||
: (localStorage.getItem('background') !== 'false');
|
||||
|
||||
if (toggleBgSwitch && bgEnabled) {
|
||||
toggleBgSwitch.classList.add('active');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user