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
+229 -13
View File
@@ -7486,6 +7486,112 @@ a.removetag i {
vertical-align: middle;
}
/* Tag Badge & Hover-to-Exclude Action */
.tag-badge {
display: inline-flex;
align-items: center;
position: relative;
transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
vertical-align: middle;
}
.tag-badge a.tag-name {
color: inherit;
text-decoration: none;
transition: opacity 0.15s ease;
}
.tag-badge .tag-exclude-btn {
background: transparent;
border: none;
padding: 0;
margin: 0;
cursor: pointer;
color: inherit;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.8em;
line-height: 1;
vertical-align: middle;
border-radius: 50%;
outline: none;
transition: all 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@media (hover: hover) {
.tag-badge:not(.tag-is-excluded) .tag-exclude-btn {
opacity: 0;
width: 0;
max-width: 0;
margin-left: 0;
overflow: hidden;
transform: scale(0.5);
pointer-events: none;
}
.tag-badge:hover .tag-exclude-btn,
.tag-badge:focus-within .tag-exclude-btn {
opacity: 0.85;
width: 14px;
max-width: 14px;
margin-left: 5px;
overflow: visible;
transform: scale(1);
pointer-events: auto;
}
}
@media (hover: none) {
.tag-badge .tag-exclude-btn {
opacity: 0.7;
width: 14px;
max-width: 14px;
margin-left: 4px;
overflow: visible;
pointer-events: auto;
transform: scale(1);
}
}
.tag-badge .tag-exclude-btn:hover,
.tag-badge .tag-exclude-btn:focus-visible {
opacity: 1;
color: #ff4757 !important;
transform: scale(1.28);
filter: drop-shadow(0 0 6px rgba(255, 71, 87, 0.75));
}
/* Excluded Tag State */
.tag-badge.tag-is-excluded {
opacity: 0.75;
box-shadow: inset 0 0 0 1px rgba(255, 71, 87, 0.55), 0 0 8px rgba(255, 71, 87, 0.2);
background-color: rgba(255, 71, 87, 0.15) !important;
}
.tag-badge.tag-is-excluded a.tag-name {
text-decoration: line-through;
text-decoration-color: rgba(255, 71, 87, 0.8);
text-decoration-thickness: 1.5px;
opacity: 0.85;
}
.tag-badge.tag-is-excluded .tag-exclude-btn {
opacity: 0.95;
width: 14px;
max-width: 14px;
margin-left: 5px;
overflow: visible;
pointer-events: auto;
transform: scale(1);
color: #ff4757;
}
.tag-badge.tag-is-excluded .tag-exclude-btn:hover {
color: #2ed573 !important;
filter: drop-shadow(0 0 6px rgba(46, 213, 115, 0.75));
}
.badge-greentext {
color: #789922;
text-shadow: inherit !important;
@@ -10106,6 +10212,26 @@ input#s_avatar {
cursor: pointer;
}
.nav-mime-item.locked {
cursor: not-allowed !important;
opacity: 0.85;
}
.nav-mime-item.locked:hover {
background: transparent !important;
color: #ccc !important;
}
.nav-mime-item.locked input[type="checkbox"] {
cursor: not-allowed !important;
pointer-events: none !important;
}
.filter-pill.locked {
cursor: not-allowed !important;
opacity: 0.85;
}
.nav-mime-divider {
height: 1px;
background: rgba(255, 255, 255, 0.1);
@@ -11715,23 +11841,19 @@ html[theme="f0ck95d"] .badge-dark {
vertical-align: middle;
text-decoration: none;
cursor: pointer;
transition: color 0.15s ease, transform 0.12s cubic-bezier(0.2, 0, 0, 1), opacity 0.15s ease;
transition: opacity 0.15s ease;
}
.steuerung a:hover,
.steuerung button:hover {
color: var(--accent);
opacity: 0.85;
transform: translateY(-1px);
opacity: 0.8;
}
.steuerung a:active,
.steuerung button:active,
.steuerung a.is-clicked,
.steuerung button.is-clicked {
transform: scale(0.92) translateY(1px);
opacity: 0.65;
transition-duration: 0.05s;
opacity: 0.6;
}
.steuerung.steuerung-icon {
@@ -11745,7 +11867,7 @@ html[theme="f0ck95d"] .badge-dark {
align-items: center;
justify-content: center;
width: 1.6em;
transition: color 0.15s ease, transform 0.12s cubic-bezier(0.2, 0, 0, 1), opacity 0.15s ease;
transition: opacity 0.15s ease;
background: none;
border: none;
padding: 0;
@@ -11755,17 +11877,14 @@ html[theme="f0ck95d"] .badge-dark {
.steuerung.steuerung-icon a:hover,
.steuerung.steuerung-icon button:hover {
color: var(--accent);
transform: translateY(-1px);
opacity: 0.8;
}
.steuerung.steuerung-icon a:active,
.steuerung.steuerung-icon button:active,
.steuerung.steuerung-icon a.is-clicked,
.steuerung.steuerung-icon button.is-clicked {
transform: scale(0.88) translateY(1px);
opacity: 0.65;
transition-duration: 0.05s;
opacity: 0.6;
}
html[theme='light'] .steuerung a,
@@ -12503,6 +12622,11 @@ input:checked+.slider:before {
grid-column: 1;
}
.rating-selector.locked {
cursor: not-allowed;
opacity: 0.9;
}
.rating-toggle-btn {
display: inline-block;
background: transparent;
@@ -12528,6 +12652,12 @@ input:checked+.slider:before {
background: rgba(255, 255, 255, 0.08);
}
.rating-toggle-btn.locked {
cursor: not-allowed !important;
pointer-events: none !important;
opacity: 0.85 !important;
}
/* Per-rating active colours */
.rating-toggle-btn.active[data-rating="sfw"] {
background: #2e7d32;
@@ -15670,6 +15800,41 @@ span.gchat-post-card--loading {
gap: 5px;
}
/* Excluded Tags Filter Modal Section */
.excluded-tags-section-header {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-bottom: 12px;
}
.excluded-tags-section-title {
font-size: 1.05em;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
letter-spacing: 0.5px;
display: inline-flex;
align-items: center;
gap: 7px;
}
.excluded-tags-section-title i {
color: #ff4757;
font-size: 0.9em;
}
.excluded-tags-count-badge {
background: #ff4757;
color: #fff;
font-size: 0.72em;
font-weight: 700;
padding: 2px 7px;
border-radius: 12px;
line-height: 1.2;
box-shadow: 0 2px 6px rgba(255, 71, 87, 0.4);
}
#nav_excluded_tags_list {
margin-bottom: 15px;
display: flex;
@@ -15677,6 +15842,57 @@ span.gchat-post-card--loading {
gap: 8px;
justify-content: center;
max-width: 100%;
min-height: 28px;
align-items: center;
}
.excluded-tag-chip {
padding: 5px 12px;
border-radius: 20px;
background: rgba(255, 71, 87, 0.12);
border: 1px solid rgba(255, 71, 87, 0.35);
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.9em;
color: #f1f2f6;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.excluded-tag-chip:hover {
background: rgba(255, 71, 87, 0.2);
border-color: rgba(255, 71, 87, 0.55);
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(255, 71, 87, 0.25);
}
.excluded-tag-chip .remove-excluded-tag {
color: #ff4757;
text-decoration: none;
font-weight: bold;
font-size: 1.15em;
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 50%;
transition: all 0.2s ease;
cursor: pointer;
}
.excluded-tag-chip .remove-excluded-tag:hover {
color: #fff;
background: #ff4757;
transform: scale(1.18);
}
.no-excluded-tags-hint {
color: rgba(255, 255, 255, 0.4);
font-size: 0.9em;
font-style: italic;
}
.nav-exclude {
+53 -35
View File
@@ -99,7 +99,7 @@
if (canManage) {
span.classList.add('can-cycle');
}
} else if (tag.display_name || tag.user) {
} else if (!window.f0ckSession?.is_anonymized && window.f0ckSession?.logged_in && !window.f0ckSession?.is_anon && (tag.display_name || tag.user)) {
span.setAttribute('tooltip', tag.display_name || tag.user);
}
@@ -226,6 +226,13 @@
const toggleFavEvent = async (e) => {
if (e && typeof e.preventDefault === 'function') e.preventDefault();
if (window.f0ckSession?.is_anon && window.f0ckSession?.anon_permissions && window.f0ckSession.anon_permissions.favorite === false) {
if (typeof window.flashMessage === 'function') {
window.flashMessage('Anonymous favoriting is disabled.', 3000, 'error');
}
return;
}
const ctx = getContext();
if (!ctx) return;
const { postid } = ctx;
@@ -234,46 +241,57 @@
const favoBtn = document.querySelector("#a_favo");
const wasAlreadyFav = favoBtn && favoBtn.classList.contains('fa-solid');
const res = await post('/api/v2/togglefav', {
postid: postid
});
if (res.success) {
if (window.invalidateItemCache) {
window.invalidateItemCache(postid);
}
// New state is the logical opposite of what it was before the API call
const isNowFav = !wasAlreadyFav;
try {
const res = await post('/api/v2/togglefav', {
postid: postid
});
if (res && res.success) {
if (window.invalidateItemCache) {
window.invalidateItemCache(postid);
}
// New state is the logical opposite of what it was before the API call
const isNowFav = !wasAlreadyFav;
if (favoBtn) {
favoBtn.classList.toggle('fa-solid', isNowFav);
favoBtn.classList.toggle('fa-regular', !isNowFav);
}
if (favoBtn) {
favoBtn.classList.toggle('fa-solid', isNowFav);
favoBtn.classList.toggle('fa-regular', !isNowFav);
}
const favcontainer = document.querySelector('#favs');
favcontainer.innerHTML = "";
if (res.favs.length > 0) {
res.favs.forEach(f => {
const a = document.createElement('a');
a.href = `/user/${f.user}`;
a.setAttribute('tooltip', f.display_name || f.user);
a.setAttribute('flow', 'up');
const favcontainer = document.querySelector('#favs');
favcontainer.innerHTML = "";
if (res.favs && res.favs.length > 0) {
res.favs.forEach(f => {
const a = document.createElement('a');
a.href = `/user/${f.user}`;
a.setAttribute('tooltip', f.display_name || f.user);
a.setAttribute('flow', 'up');
const img = document.createElement('img');
img.src = f.avatar_file ? `/a/${f.avatar_file}` : (f.avatar ? `/t/${f.avatar}.webp` : '/a/default.png');
img.style.height = "32px";
img.style.width = "32px";
if (f.username_color) img.style.borderColor = f.username_color;
const img = document.createElement('img');
img.src = f.avatar_file ? `/a/${f.avatar_file}` : (f.avatar ? `/t/${f.avatar}.webp` : '/a/default.png');
img.style.height = "32px";
img.style.width = "32px";
if (f.username_color) img.style.borderColor = f.username_color;
a.appendChild(img);
favcontainer.appendChild(a);
});
favcontainer.hidden = false;
a.appendChild(img);
favcontainer.appendChild(a);
});
favcontainer.hidden = false;
} else {
favcontainer.hidden = true;
}
window.flashMessage((window.f0ckI18n && (isNowFav ? window.f0ckI18n.fav_added : window.f0ckI18n.fav_removed)) || (isNowFav ? 'ADDED TO FAVORITES' : 'REMOVED FROM FAVORITES'));
if (navigator.vibrate) navigator.vibrate(50);
} else {
favcontainer.hidden = true;
const errMsg = (res && (res.msg || res.error)) || 'Anonymous favoriting is disabled.';
if (typeof window.flashMessage === 'function') {
window.flashMessage(errMsg, 3000, 'error');
}
}
} catch (err) {
if (typeof window.flashMessage === 'function') {
window.flashMessage('Failed to update favorite.', 3000, 'error');
}
window.flashMessage((window.f0ckI18n && (isNowFav ? window.f0ckI18n.fav_added : window.f0ckI18n.fav_removed)) || (isNowFav ? 'ADDED TO FAVORITES' : 'REMOVED FROM FAVORITES'));
if (navigator.vibrate) navigator.vibrate(50);
}
};
+141 -17
View File
@@ -84,6 +84,9 @@
this.rawSeed = null;
this.isSessionReady = false;
this.hwFingerprint = null;
try {
this.hwFingerprint = localStorage.getItem('f0ck_anon_hw_fp') || null;
} catch (e) {}
}
/**
@@ -92,6 +95,13 @@
*/
async getHardwareFingerprint() {
if (this.hwFingerprint) return this.hwFingerprint;
try {
const cached = localStorage.getItem('f0ck_anon_hw_fp');
if (cached) {
this.hwFingerprint = cached;
return this.hwFingerprint;
}
} catch (e) {}
try {
let glVendor = '';
@@ -117,16 +127,67 @@
}
} catch (e) {}
// WebGPU adapter architecture (modern GPUs)
let gpuArch = '';
try {
if (navigator.gpu) {
const adapter = await navigator.gpu.requestAdapter();
if (adapter && adapter.info) {
gpuArch = [adapter.info.architecture, adapter.info.vendor, adapter.info.description].filter(Boolean).join(':');
}
}
} catch (e) {}
// CPU & Memory
const concurrency = navigator.hardwareConcurrency || 0;
const memory = navigator.deviceMemory || 0;
const platform = navigator.platform || '';
// Display, Gamut & Dynamic Range
const screenInfo = [
window.screen ? window.screen.width : 0,
window.screen ? window.screen.height : 0,
window.screen ? window.screen.availWidth : 0,
window.screen ? window.screen.availHeight : 0,
window.screen ? window.screen.colorDepth : 0,
window.screen ? window.screen.pixelDepth : 0,
window.devicePixelRatio || 1
window.devicePixelRatio || 1,
window.matchMedia && window.matchMedia('(color-gamut: p3)').matches ? 'p3' : (window.matchMedia && window.matchMedia('(color-gamut: rec2020)').matches ? 'rec2020' : 'srgb'),
window.matchMedia && window.matchMedia('(dynamic-range: high)').matches ? 'hdr' : 'sdr'
].join('x');
// Input & Peripheral Hardware (touch, mouse, stylus)
const touchPoints = (typeof navigator !== 'undefined' && 'maxTouchPoints' in navigator) ? navigator.maxTouchPoints : 0;
const pointerType = (window.matchMedia && window.matchMedia('(pointer: fine)').matches) ? 'fine' : ((window.matchMedia && window.matchMedia('(pointer: coarse)').matches) ? 'coarse' : 'none');
const hoverType = (window.matchMedia && window.matchMedia('(hover: hover)').matches) ? 'hover' : 'none';
const inputHardware = `${touchPoints}:${pointerType}:${hoverType}`;
// 2D Canvas Font Rasterizer & Subpixel Geometry
let canvasFp = '';
try {
const c2d = document.createElement('canvas');
c2d.width = 240;
c2d.height = 60;
const ctx = c2d.getContext('2d');
if (ctx) {
ctx.textBaseline = 'alphabetic';
ctx.fillStyle = '#f60';
ctx.fillRect(10, 5, 60, 20);
ctx.fillStyle = '#069';
ctx.font = '14pt Arial, sans-serif';
ctx.fillText('f0ck.dev <canvas> 😃', 4, 35);
ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';
ctx.font = '16pt Times, serif';
ctx.fillText('f0ck.dev <canvas> 😃', 20, 48);
const imgData = ctx.getImageData(0, 0, 240, 60).data;
let sum = 0;
for (let i = 0; i < imgData.length; i += 4) {
sum = (sum * 31 + imgData[i] + imgData[i+1] + imgData[i+2] + imgData[i+3]) >>> 0;
}
canvasFp = sum.toString(16);
}
} catch (e) {}
// Web Audio DSP Floating-Point Math
let audioFp = '';
try {
const AudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
@@ -159,14 +220,26 @@
}
} catch (e) {}
// FPU Math Microarchitecture Precision
const mathPrecision = [
Math.tan(-1e300).toString().substring(0, 10),
Math.sinh(1).toString().substring(0, 10),
Math.acos(0.123456789).toString().substring(0, 10)
].join(';');
const rawHardware = [
glVendor,
glRenderer,
glLimits,
gpuArch,
concurrency,
memory,
platform,
screenInfo,
audioFp
inputHardware,
canvasFp,
audioFp,
mathPrecision
].join('~~~');
const encoder = new TextEncoder();
@@ -174,6 +247,9 @@
const hashBuf = await window.crypto.subtle.digest('SHA-256', data);
const hashHex = bytesToHex(new Uint8Array(hashBuf));
this.hwFingerprint = `HW:${hashHex}`;
try {
localStorage.setItem('f0ck_anon_hw_fp', this.hwFingerprint);
} catch (e) {}
return this.hwFingerprint;
} catch (err) {
console.warn('[ANON_SSH] Failed to compute hardware fingerprint:', err);
@@ -483,7 +559,7 @@
localStorage.setItem(STORAGE_KEY_PUB, this.pubkey);
localStorage.setItem(STORAGE_KEY_FP, this.fingerprint);
await this.ensureSession(true);
await this.ensureSession(true, isExplicitLogin);
return this.getIdentity();
}
@@ -514,7 +590,11 @@
/**
* Authenticate to backend and establish/refresh anonymous session
*/
async ensureSession(force = false) {
async ensureSession(force = false, isExplicitLogin = false) {
if (window.location.pathname === '/banned') {
return;
}
// If user is already logged in as a real registered user, don't overwrite session!
if (window.f0ckSession && window.f0ckSession.user && !window.f0ckSession.is_anon) {
return;
@@ -550,8 +630,15 @@
reason: data.reason,
expires: data.expires
});
if (window.location.pathname !== '/banned') {
window.location.href = data.redirect || '/banned';
this.clearStoredIdentity();
this.updateNavUI(false);
// ONLY redirect to /banned if user was explicitly trying to login!
if (isExplicitLogin) {
const anonModal = document.getElementById('anon-ssh-modal');
if (anonModal) anonModal.style.display = 'none';
if (window.location.pathname !== '/banned') {
window.location.href = data.redirect || '/banned';
}
}
return;
}
@@ -594,6 +681,9 @@
setTombstone(tombstone) {
try {
localStorage.setItem('f0ck_anon_tombstone', JSON.stringify(tombstone));
if (tombstone && tombstone.banned) {
document.cookie = `f0ck_banned=${encodeURIComponent(JSON.stringify(tombstone))}; Path=/; Max-Age=31536000; SameSite=Lax`;
}
} catch (e) {}
}
@@ -653,6 +743,7 @@
localStorage.removeItem(STORAGE_KEY_PRIV);
localStorage.removeItem(STORAGE_KEY_PUB);
localStorage.removeItem(STORAGE_KEY_FP);
document.cookie = 'f0ck_banned=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax';
this.cryptoKey = null;
this.rawSeed = null;
this.rawPub = null;
@@ -669,11 +760,27 @@
if (window.f0ckSession && window.f0ckSession.enable_anonymous_access === false) {
return;
}
// If client has a ban tombstone, redirect to /banned directly on login attempt
const tombstone = this.getTombstone();
if (tombstone && tombstone.banned) {
const anonModal = document.getElementById('anon-ssh-modal');
if (anonModal) anonModal.style.display = 'none';
if (window.location.pathname !== '/banned') {
window.location.href = '/banned';
}
return;
}
try {
if (!this.pubkey) {
await this.generateIdentity();
await this.generateIdentity(true);
} else {
await this.ensureSession(true, true);
}
await this.ensureSession(true);
if (!this.isSessionReady) return;
// Sync any guest favorites saved in localStorage to this anon account
if (window.f0ckGuestFavs && typeof window.f0ckGuestFavs.importToAccount === 'function') {
await window.f0ckGuestFavs.importToAccount();
@@ -698,12 +805,16 @@
this.clearStoredIdentity();
await fetch('/api/v2/anon/logout', { method: 'POST', credentials: 'same-origin' }).catch(() => {});
} finally {
window.location.href = '/';
this.updateNavUI(false);
if (typeof window.showToastNotification === 'function') {
window.showToastNotification('Logged out from anonymous session');
}
window.location.reload();
}
}
/**
* Update visitor navbar UI elements
* Update navigation bar elements based on anonymous authentication state
*/
updateNavUI(isAnon) {
const icon = document.getElementById('nav-visitor-icon');
@@ -765,8 +876,12 @@
* Initialize on page startup
*/
async init() {
if (window.location.pathname === '/banned') {
return;
}
if (window.f0ckSession && window.f0ckSession.enable_anonymous_access === false) {
if (window.f0ckSession.is_anon) {
if (window.f0ckSession.is_anon && window.f0ckSession.logged_in) {
await fetch('/api/v2/anon/logout', { method: 'POST', credentials: 'same-origin' }).catch(() => {});
window.location.reload();
return;
@@ -780,6 +895,15 @@
return;
}
// If client has a ban tombstone, do NOT attempt background session handshake on page load/reload!
const tombstone = this.getTombstone();
if (tombstone && tombstone.banned) {
this.clearStoredIdentity();
this.updateNavUI(false);
this.attachUIListeners();
return;
}
const storedPriv = localStorage.getItem(STORAGE_KEY_PRIV);
const storedPub = localStorage.getItem(STORAGE_KEY_PUB);
const storedFp = localStorage.getItem(STORAGE_KEY_FP);
@@ -808,12 +932,12 @@
// User previously logged in as anonymous with this key
// If backend session is already active, avoid redundant session churn
if (window.f0ckSession && window.f0ckSession.is_anon) {
if (window.f0ckSession && window.f0ckSession.is_anon && window.f0ckSession.logged_in) {
this.isSessionReady = true;
this.updateNavUI(true);
} else {
await this.ensureSession();
this.updateNavUI(true);
// Unauthenticated guest state: do NOT auto-login as anonymous on page load!
this.updateNavUI(false);
}
} catch (err) {
console.warn('[ANON_SSH] Failed to restore stored key:', err);
@@ -822,7 +946,7 @@
}
} else {
// Clean guest state! Do NOT auto-generate or establish session!
if (window.f0ckSession && window.f0ckSession.is_anon) {
if (window.f0ckSession && window.f0ckSession.is_anon && window.f0ckSession.logged_in) {
// Stale backend anon session cookie without corresponding local key: clear cookie
await fetch('/api/v2/anon/logout', { method: 'POST', credentials: 'same-origin' }).catch(() => {});
window.location.reload();
@@ -853,7 +977,7 @@
}
const logoutTarget = e.target.closest('#nav-anon-logout-btn, a[href="/logout"]');
if (logoutTarget && ((window.f0ckSession && window.f0ckSession.is_anon) || this.pubkey)) {
if (logoutTarget && ((window.f0ckSession && window.f0ckSession.is_anon && window.f0ckSession.logged_in) || this.pubkey)) {
e.preventDefault();
this.logoutAnonymous();
return;
+8 -2
View File
@@ -2230,7 +2230,7 @@ class CommentSystem {
const authorUserId = comment.user_id ?? (comment.username && window.f0ckSession && comment.username.toLowerCase() === (window.f0ckSession.user || '').toLowerCase() ? (window.f0ckSession.id || window.f0ckSession.user_id) : null);
const authorUsernameColor = comment.username_color || (comment.username && window.f0ckSession && comment.username.toLowerCase() === (window.f0ckSession.user || '').toLowerCase() ? window.f0ckSession.username_color : null);
const isAnonGuest = window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in;
const isAnonGuest = window.f0ckSession?.is_anonymized ?? (window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in);
if (isAnonGuest) bannerStyle = '';
const avatarHtml = isAnonGuest
@@ -2366,7 +2366,8 @@ class CommentSystem {
const counter = (maxLen !== null && maxLen !== undefined)
? `<span class="char-counter" data-max="${maxLen}">0 / ${maxLen}</span>`
: '';
const fileUploadEnabled = session.logged_in && session.allow_fileupload_comments;
const anonAttachmentsDisabled = session.is_anon && session.anon_permissions?.comment_attachments === false;
const fileUploadEnabled = session.logged_in && session.allow_fileupload_comments && !anonAttachmentsDisabled;
const multiFile = session.fileupload_comments_multifile;
const attachBtn = fileUploadEnabled
? `<button class="comment-attach-btn" title="${attachLabel}" type="button"><i class="fa-solid fa-paperclip"></i></button><input type="file" class="comment-file-input" accept="image/*,video/*,audio/*" ${multiFile ? 'multiple' : ''} style="display:none;">`
@@ -3350,6 +3351,11 @@ class CommentSystem {
return;
}
if (submitBtn.classList.contains('loading') || submitBtn.disabled) return;
if (window.f0ckSession?.is_anon && window.f0ckSession?.anon_permissions?.comment === false) {
if (window.flashMessage) window.flashMessage('Anonymous commenting is disabled.', 3000, 'error');
else alert('Anonymous commenting is disabled.');
return;
}
// ── Upload all staged files now (at submit time) ───────────────────────
const fileIds = [];
+680 -100
View File
File diff suppressed because it is too large Load Diff
+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]);
+23 -5
View File
@@ -562,7 +562,7 @@
? `style="--author-banner: url('/a/${bannerFile}'); --author-banner-position: ${bannerPos === 'center' ? 'center top' : (bannerPos || 'center top')}; --author-banner-size: ${(bannerSz && bannerSz !== 'cover') ? bannerSz : '100% auto'}; --author-banner-repeat: no-repeat;"`
: '';
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 effectiveBannerStyle = isAnonGuest ? '' : bannerStyle;
const authorAvatarHtml = isAnonGuest
? `<span class="sidebar-avatar-link"><img src="/a/default.png" class="sidebar-avatar" loading="eager" /></span>`
@@ -1031,13 +1031,31 @@
let lastBoundMode = typeof window.activeMode !== 'undefined' ? window.activeMode : null;
const getCurrentMimeFilter = () => {
const allowed = window.f0ckSession?.is_anon && Array.isArray(window.f0ckSession?.anon_permissions?.allowed_mimes) ? window.f0ckSession.anon_permissions.allowed_mimes : null;
const urlParams = new URLSearchParams(window.location.search);
const qMime = urlParams.get('mime');
if (qMime !== null) return qMime.trim();
let qMime = urlParams.get('mime');
if (qMime !== null) {
qMime = qMime.trim();
if (allowed) {
const parts = qMime.split(',').filter(m => allowed.includes(m));
return parts.length > 0 ? parts.join(',') : (allowed.length < 5 ? allowed.join(',') : '');
}
return qMime;
}
const cookieMime = document.cookie.split('; ').find(row => row.startsWith('mime='));
if (cookieMime) {
const val = cookieMime.split('=')[1];
if (typeof val === 'string') return decodeURIComponent(val).trim();
if (typeof val === 'string') {
const cMime = decodeURIComponent(val).trim();
if (allowed) {
const parts = cMime.split(',').filter(m => allowed.includes(m));
return parts.length > 0 ? parts.join(',') : (allowed.length < 5 ? allowed.join(',') : '');
}
return cMime;
}
}
if (allowed && allowed.length < 5) {
return allowed.join(',');
}
return '';
};
@@ -1176,7 +1194,7 @@
displayTitle = `${videoKey}`;
}
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 authorName = isAnonGuest ? 'anonymous' : (video.display_name || video.username);
const userColorStyle = (!isAnonGuest && video.username_color) ? `style="color: ${escapeHtml(video.username_color)}"` : '';
+58 -42
View File
@@ -95,7 +95,7 @@
if (canManage) {
span.classList.add('can-cycle');
}
} else if (tag.display_name || tag.user) {
} else if (!window.f0ckSession?.is_anonymized && window.f0ckSession?.logged_in && !window.f0ckSession?.is_anon && (tag.display_name || tag.user)) {
span.setAttribute('tooltip', tag.display_name || tag.user);
}
@@ -196,6 +196,13 @@
const toggleFavEvent = async (e) => {
// e is the click event or undefined
if (e && typeof e.preventDefault === 'function') e.preventDefault();
if (window.f0ckSession?.is_anon && window.f0ckSession?.anon_permissions && window.f0ckSession.anon_permissions.favorite === false) {
if (typeof window.flashMessage === 'function') {
window.flashMessage('Anonymous favoriting is disabled.', 3000, 'error');
}
return;
}
const ctx = getContext();
if (!ctx) return;
const { postid } = ctx;
@@ -204,50 +211,59 @@
const favoBtn = document.querySelector("#a_favo");
const wasAlreadyFav = favoBtn && favoBtn.classList.contains('fa-solid');
const res = await post('/api/v2/togglefav', {
postid: postid
});
if (res.success) {
if (window.invalidateItemCache) {
window.invalidateItemCache(postid);
try {
const res = await post('/api/v2/togglefav', {
postid: postid
});
if (res && res.success) {
if (window.invalidateItemCache) {
window.invalidateItemCache(postid);
}
// New state is the logical opposite of what it was before the API call
const isNowFav = !wasAlreadyFav;
if (favoBtn) {
favoBtn.classList.toggle('fa-solid', isNowFav);
favoBtn.classList.toggle('fa-regular', !isNowFav);
}
// span#favs
const favcontainer = document.querySelector('#favs');
favcontainer.innerHTML = "";
if (res.favs && res.favs.length > 0) {
res.favs.forEach(f => {
const a = document.createElement('a');
a.href = `/user/${f.user}`;
a.setAttribute('tooltip', f.display_name || f.user);
a.setAttribute('flow', 'up');
const img = document.createElement('img');
img.src = f.avatar_file ? `/a/${f.avatar_file}` : (f.avatar ? `/t/${f.avatar}.webp` : '/a/default.png');
img.style.height = "32px";
img.style.width = "32px";
if (f.username_color) img.style.borderColor = f.username_color;
a.appendChild(img);
favcontainer.appendChild(a);
});
favcontainer.hidden = false;
} else {
favcontainer.hidden = true;
}
window.flashMessage((window.f0ckI18n && (isNowFav ? window.f0ckI18n.fav_added : window.f0ckI18n.fav_removed)) || (isNowFav ? 'ADDED TO FAVORITES' : 'REMOVED FROM FAVORITES'));
if (navigator.vibrate) navigator.vibrate(50);
}
// New state is the logical opposite of what it was before the API call
const isNowFav = !wasAlreadyFav;
if (favoBtn) {
favoBtn.classList.toggle('fa-solid', isNowFav);
favoBtn.classList.toggle('fa-regular', !isNowFav);
else {
const errMsg = (res && (res.msg || res.error)) || 'Anonymous favoriting is disabled.';
if (typeof window.flashMessage === 'function') {
window.flashMessage(errMsg, 3000, 'error');
}
}
// span#favs
const favcontainer = document.querySelector('#favs');
favcontainer.innerHTML = "";
if (res.favs.length > 0) {
res.favs.forEach(f => {
const a = document.createElement('a');
a.href = `/user/${f.user}`;
a.setAttribute('tooltip', f.display_name || f.user);
a.setAttribute('flow', 'up');
const img = document.createElement('img');
img.src = f.avatar_file ? `/a/${f.avatar_file}` : (f.avatar ? `/t/${f.avatar}.webp` : '/a/default.png');
img.style.height = "32px";
img.style.width = "32px";
if (f.username_color) img.style.borderColor = f.username_color;
a.appendChild(img);
favcontainer.appendChild(a);
});
favcontainer.hidden = false;
} else {
favcontainer.hidden = true;
} catch (err) {
if (typeof window.flashMessage === 'function') {
window.flashMessage('Failed to update favorite.', 3000, 'error');
}
window.flashMessage((window.f0ckI18n && (isNowFav ? window.f0ckI18n.fav_added : window.f0ckI18n.fav_removed)) || (isNowFav ? 'ADDED TO FAVORITES' : 'REMOVED FROM FAVORITES'));
if (navigator.vibrate) navigator.vibrate(50);
}
else {
// lul
}
};