This commit is contained in:
2026-09-13 04:05:59 +02:00
parent 90860b9279
commit 340c825019
48 changed files with 2727 additions and 532 deletions
+76 -15
View File
@@ -97,7 +97,11 @@
let autoNextLoops = Math.max(0, parseInt(prefs.autoNextLoops ?? 1, 10));
let leftHandEnabled = prefs.leftHand === true;
let applied = { mode: defaultMode, mime: '', order: 'random', tags: [], externalUrl: null };
const isScrollerAnon = !(window.f0ckSession && window.f0ckSession.user && !window.f0ckSession.is_anon);
const scrollerAllowedMimes = window.f0ckSession?.anon_permissions?.allowed_mimes;
const scrollerSingleMime = (isScrollerAnon && Array.isArray(scrollerAllowedMimes) && scrollerAllowedMimes.length === 1) ? scrollerAllowedMimes[0] : null;
let applied = { mode: defaultMode, mime: scrollerSingleMime || '', order: 'random', tags: [], externalUrl: null };
let pending = { ...applied, tags: [] };
// Volume / mute
@@ -192,7 +196,8 @@
if (hid && !cache.items.some(item => String(item.id) === hid)) return false;
if (cache.filters) {
applied = { mode: defaultMode, mime: '', order: 'random', tags: [], ...cache.filters };
applied = { mode: defaultMode, mime: scrollerSingleMime || '', order: 'random', tags: [], ...cache.filters };
if (scrollerSingleMime) applied.mime = scrollerSingleMime;
applied.tags = Array.isArray(cache.filters.tags) ? [...cache.filters.tags] : [];
pending = { ...applied, tags: [...applied.tags] };
}
@@ -1141,7 +1146,23 @@
}
async function toggleFav(slide) {
if (!window.scrollerLoggedIn) return;
if (window.f0ckSession?.is_anon && window.f0ckSession?.anon_permissions && window.f0ckSession.anon_permissions.favorite === false) {
const errMsg = 'Anonymous favoriting is disabled.';
if (typeof window.flashMessage === 'function') {
window.flashMessage(errMsg, 3000, 'error');
} else if (typeof showShareToast === 'function') {
showShareToast(errMsg);
}
return;
}
if (!window.scrollerLoggedIn) {
if (typeof showShareToast === 'function') {
showShareToast('Login to favorite posts');
} else if (typeof window.flashMessage === 'function') {
window.flashMessage('Login to favorite posts', 3000, 'warning');
}
return;
}
const id = slide.dataset.localId || slide.dataset.id;
// External items have non-numeric IDs (e.g. "gif/123") — can't fav until rehosted
if (!/^\d+$/.test(id)) { showShareToast('Can\u2019t fav external items'); return; }
@@ -1157,10 +1178,6 @@
if (countEl) countEl.textContent = Math.max(0, (parseInt(countEl.textContent || '0', 10)) + (nowFaved ? 1 : -1));
if (nowFaved) flashFav(slide);
}
if (!window.scrollerLoggedIn && (!window.f0ckSession || !window.f0ckSession.user)) {
showShareToast('Login to favorite posts');
return;
}
try {
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
const resp = await fetch('/api/v2/togglefav', {
@@ -1171,7 +1188,24 @@
},
body: `postid=${id}${csrfToken ? `&csrf_token=${encodeURIComponent(csrfToken)}` : ''}`
});
const data = await resp.json();
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data.success) {
// Rollback optimistic update
if (favBtn) {
favBtn.classList.toggle('faved', wasFaved);
const icon = favBtn.querySelector('i');
if (icon) icon.className = (wasFaved ? 'fa-solid' : 'fa-regular') + ' fa-heart';
const countEl = favBtn.querySelector('.scroll-btn-count');
if (countEl) countEl.textContent = Math.max(0, (parseInt(countEl.textContent || '0', 10)) + (wasFaved ? 0 : -1));
}
const errMsg = (data && (data.msg || data.error)) || 'Anonymous favoriting is disabled.';
if (typeof window.flashMessage === 'function') {
window.flashMessage(errMsg, 3000, 'error');
} else if (typeof showShareToast === 'function') {
showShareToast(errMsg);
}
return;
}
// Sync count to server truth (handles race conditions)
if (data.success && favBtn && data.favs) {
const countEl = favBtn.querySelector('.scroll-btn-count');
@@ -1183,6 +1217,8 @@
favBtn.classList.toggle('faved', wasFaved);
const icon = favBtn.querySelector('i');
if (icon) icon.className = (wasFaved ? 'fa-solid' : 'fa-regular') + ' fa-heart';
const countEl = favBtn.querySelector('.scroll-btn-count');
if (countEl) countEl.textContent = Math.max(0, (parseInt(countEl.textContent || '0', 10)) + (wasFaved ? 0 : -1));
}
}
}
@@ -1349,7 +1385,7 @@
const meta = document.createElement('div'); meta.className = 'scroll-meta';
// esc() the color value: a raw '"' in username_color would break out of the
// style attribute and allow arbitrary HTML injection (XSS).
const isAnonGuest = window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in;
const isAnonGuest = window.f0ckSession?.is_anonymized ?? (window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in);
const colorStyle = (!isAnonGuest && item.username_color) ? `color:${esc(item.username_color)}` : '';
const ratingHtml = `<span class="scroll-rating ${esc(item.rating_class)}" data-item-id="${item.id}" data-rating="${esc(item.rating_class)}">${esc(item.rating_label)}</span>`;
const ocHtml = item.is_oc ? `<span class="scroll-oc"><i class="fa-solid fa-star" style="font-size:.6rem"></i> OC</span>` : '';
@@ -2176,7 +2212,7 @@
function renderCommentEl(c, canReply) {
const el = document.createElement('div'); el.className = 'comment-item';
el.dataset.commentId = c.id || '';
const isAnonGuest = window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in;
const isAnonGuest = window.f0ckSession?.is_anonymized ?? (window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in);
const av = isAnonGuest ? '/a/default.png' : (c.avatar_file ? `/a/${c.avatar_file}` : (c.avatar ? `/t/${c.avatar}.webp` : '/a/default.png'));
const nc = (!isAnonGuest && c.username_color) ? `color:${esc(c.username_color)}` : '';
const _i = window.f0ckI18n || {};
@@ -3003,6 +3039,10 @@
});
function syncPanelUI() {
if (scrollerSingleMime) {
pending.mime = scrollerSingleMime;
applied.mime = scrollerSingleMime;
}
document.querySelectorAll('#mode-pills .filter-pill').forEach(p => p.classList.toggle('active', +p.dataset.mode === pending.mode));
document.querySelectorAll('#mime-pills .filter-pill').forEach(p => p.classList.toggle('active', p.dataset.mime === pending.mime));
document.querySelectorAll('#order-pills .filter-pill').forEach(p => p.classList.toggle('active', p.dataset.order === pending.order));
@@ -3011,9 +3051,15 @@
}
function makePillListener(groupId, key, transform) {
document.getElementById(groupId).querySelectorAll('.filter-pill').forEach(pill => {
pill.addEventListener('click', () => {
document.getElementById(groupId).querySelectorAll('.filter-pill').forEach(p => p.classList.remove('active'));
const el = document.getElementById(groupId);
if (!el) return;
el.querySelectorAll('.filter-pill').forEach(pill => {
pill.addEventListener('click', (e) => {
if (groupId === 'mime-pills' && scrollerSingleMime) {
e.preventDefault();
return;
}
el.querySelectorAll('.filter-pill').forEach(p => p.classList.remove('active'));
pill.classList.add('active'); pending[key] = transform ? transform(pill.dataset[key]) : pill.dataset[key];
});
});
@@ -3078,7 +3124,7 @@
}
}
filterResetBtn.addEventListener('click', () => { pending = { mode: defaultMode, mime: '', order: 'random', tags: [] }; syncPanelUI(); tagInput.value = ''; tagClear.classList.remove('show'); tagSuggestEl.innerHTML = ''; lastSugg = []; renderActiveTags(); });
filterResetBtn.addEventListener('click', () => { pending = { mode: defaultMode, mime: scrollerSingleMime || '', order: 'random', tags: [] }; syncPanelUI(); tagInput.value = ''; tagClear.classList.remove('show'); tagSuggestEl.innerHTML = ''; lastSugg = []; renderActiveTags(); });
function updateFilterSummary() {
const is4chan = !!applied.externalUrl;
@@ -3500,11 +3546,26 @@
const initScrollerSSE = () => {
if (sseEs) sseEs.close();
sseEs = new EventSource(`/api/notifications/stream?tabId=${tabId}`);
let sseUrl = `/api/notifications/stream?tabId=${tabId}`;
try {
const fp = localStorage.getItem('f0ck_anon_ssh_fp');
if (fp) sseUrl += `&fp=${encodeURIComponent(fp)}`;
const hw = localStorage.getItem('f0ck_anon_hw_fp');
if (hw) sseUrl += `&hw=${encodeURIComponent(hw)}`;
} catch (e) {}
sseEs = new EventSource(sseUrl);
sseEs.onopen = () => { sseRetryCount = 0; };
sseEs.onmessage = (e) => {
try {
const data = JSON.parse(e.data);
if (data.type === 'banned') {
try {
const tombstoneData = { banned: true, reason: data.data?.reason, expires: data.data?.expires };
document.cookie = `f0ck_banned=${encodeURIComponent(JSON.stringify(tombstoneData))}; Path=/; Max-Age=31536000; SameSite=Lax`;
} catch (err) {}
window.location.href = data.data?.redirect || '/banned';
return;
}
if (data.type === 'notify') {
pollNotifCount(); // instant re-fetch on SSE push
if (navigator.vibrate) navigator.vibrate([200, 80, 200]);