This commit is contained in:
2026-09-26 15:23:36 +02:00
parent 990f463534
commit 27f5e3563f
18 changed files with 1226 additions and 559 deletions
+249 -157
View File
@@ -284,6 +284,8 @@
};
let favSeq = 0;
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) {
@@ -300,7 +302,12 @@
const chanUrl = firstFavoBtn?.dataset?.chanUrl || chanRehostBtn?.dataset?.url;
let localId = firstFavoBtn?.dataset?.localId;
const ctx = getContext();
const postid = localId ? Number(localId) : ctx?.postid;
if (!postid && !chanUrl) return;
const wasAlreadyFav = favoBtns[0].classList.contains('fa-solid') && !favoBtns[0].classList.contains('fa-spinner');
const isNowFav = !wasAlreadyFav;
const setFavoUi = (isFav) => {
favoBtns.forEach(btn => {
@@ -308,178 +315,263 @@
btn.classList.add('iconset', 'fa-heart');
btn.classList.toggle('fa-solid', isFav);
btn.classList.toggle('fa-regular', !isFav);
btn.title = isFav ? (window.f0ckI18n?.fav_remove || 'Remove from favorites') : (window.f0ckI18n?.fav_add || 'Favorite');
});
};
const setFavoLoading = () => {
favoBtns.forEach(btn => {
btn.classList.remove('fa-heart');
btn.classList.add('iconset', 'fa-solid', 'fa-spinner', 'fa-spin');
});
};
// 1. Instantly apply client UI
setFavoUi(isNowFav);
// If viewing un-rehosted 4chan post, auto-rehost first
if (isChan && !localId && chanUrl) {
setFavoLoading();
try {
const csrfToken = window.f0ckSession?.csrf_token || '';
const rehostResp = await fetch('/api/v2/scroller/rehost', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
},
body: new URLSearchParams({
url: chanUrl,
original_filename: firstFavoBtn?.dataset?.filename || chanRehostBtn?.dataset?.filename || '',
width: firstFavoBtn?.dataset?.width || chanRehostBtn?.dataset?.width || '',
height: firstFavoBtn?.dataset?.height || chanRehostBtn?.dataset?.height || ''
})
});
const rehostData = await rehostResp.json();
if (rehostData.success && rehostData.item_id) {
localId = rehostData.item_id;
favoBtns.forEach(btn => {
btn.dataset.itemId = localId;
btn.dataset.localId = localId;
});
const commentsEl = document.querySelector("#comments-container");
if (commentsEl) commentsEl.dataset.itemId = localId;
const infoEl = document.querySelector("#a_info");
if (infoEl) infoEl.dataset.itemId = localId;
// Micro-animation for tactile pop
favoBtns.forEach(btn => {
btn.classList.remove('fav-pop');
void btn.offsetWidth;
btn.classList.add('fav-pop');
});
const timeEl = document.querySelector('time.timeago');
if (timeEl) {
const nowIso = new Date().toISOString();
timeEl.dataset.iso = nowIso;
const fullDate = typeof window.f0ckFormatDateFull === 'function' ? window.f0ckFormatDateFull(nowIso) : new Date().toLocaleString();
timeEl.setAttribute('tooltip', fullDate);
timeEl.textContent = (window.f0ckTimeAgo ? window.f0ckTimeAgo(nowIso) : (window.f0ckI18n?.timeago_just_now || 'just now'));
// Instant flash message & vibration feedback
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);
if (postid && window.invalidateItemCache) {
window.invalidateItemCache(postid);
}
// Optimistically update #favs badge list
const favcontainer = document.querySelector('#favs');
const prevFavsHtml = favcontainer ? favcontainer.innerHTML : '';
const prevFavsHidden = favcontainer ? favcontainer.hidden : true;
if (favcontainer) {
const currentUser = (window.f0ckSession?.user || '').toLowerCase();
const isAnon = !!(window.f0ckSession?.is_anon || window.f0ckSession?.user === 'anonymous');
const isAnonymized = !!(window.f0ckSession?.is_anonymized ?? (window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in) ?? (window.f0ckSession?.is_anon && window.f0ckSession?.anon_anonymize));
const findSelfBadge = () => {
const selfBadges = favcontainer.querySelectorAll('[data-self="true"]');
if (selfBadges.length) return selfBadges[0];
if (currentUser && currentUser !== 'anonymous') {
const links = favcontainer.querySelectorAll('a[href]');
for (const a of links) {
const path = a.getAttribute('href') || '';
if (path.toLowerCase().endsWith('/user/' + currentUser)) return a;
}
}
return null;
};
if (chanRehostBtn) {
chanRehostBtn.classList.add('rehosted');
chanRehostBtn.outerHTML = `
<span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;">
<a href="/${localId}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #${localId})" style="display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;"><i class="fa-solid fa-check"></i> #${localId}</a>
<button type="button" class="chan-rehost-edit-btn" data-item-id="${localId}" title="Edit rating, tags & comment" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"><i class="fa-solid fa-pen"></i></button>
</span>
`;
if (!isNowFav) {
const selfBadge = findSelfBadge();
if (selfBadge) selfBadge.remove();
if (favcontainer.children.length === 0) {
favcontainer.hidden = true;
}
} else {
let selfBadge = findSelfBadge();
if (!selfBadge) {
selfBadge = document.createElement('a');
selfBadge.dataset.self = 'true';
selfBadge.setAttribute('flow', 'up');
if (isAnonymized || isAnon) {
selfBadge.className = 'ghost-fav';
selfBadge.setAttribute('tooltip', 'anonymous');
selfBadge.style.cursor = 'default';
const img = document.createElement('img');
img.src = '/a/default.png';
img.style.height = "32px";
img.style.width = "32px";
selfBadge.appendChild(img);
} else {
selfBadge.href = `/user/${currentUser}`;
selfBadge.setAttribute('tooltip', window.f0ckSession?.display_name || window.f0ckSession?.user || 'anonymous');
const img = document.createElement('img');
const avatarFile = window.f0ckSession?.avatar_file;
const avatar = window.f0ckSession?.avatar;
img.src = avatarFile ? `/a/${avatarFile}` : (avatar ? `/t/${avatar}.webp` : '/a/default.png');
img.style.height = "32px";
img.style.width = "32px";
if (window.f0ckSession?.username_color) img.style.borderColor = window.f0ckSession.username_color;
selfBadge.appendChild(img);
}
favcontainer.appendChild(selfBadge);
}
favcontainer.hidden = false;
}
}
const mySeq = ++favSeq;
// 2. Run server operation in background — can take as long as it needs
(async () => {
try {
// If viewing un-rehosted 4chan post, auto-rehost first
if (isChan && !localId && chanUrl) {
if (mySeq !== favSeq) return;
const csrfToken = window.f0ckSession?.csrf_token || '';
const rehostResp = await fetch('/api/v2/scroller/rehost', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
},
body: new URLSearchParams({
url: chanUrl,
original_filename: firstFavoBtn?.dataset?.filename || chanRehostBtn?.dataset?.filename || '',
width: firstFavoBtn?.dataset?.width || chanRehostBtn?.dataset?.width || '',
height: firstFavoBtn?.dataset?.height || chanRehostBtn?.dataset?.height || ''
})
});
const rehostData = await rehostResp.json();
if (rehostData.success && rehostData.item_id) {
localId = rehostData.item_id;
favoBtns.forEach(btn => {
btn.dataset.itemId = localId;
btn.dataset.localId = localId;
});
const commentsEl = document.querySelector("#comments-container");
if (commentsEl) commentsEl.dataset.itemId = localId;
const infoEl = document.querySelector("#a_info");
if (infoEl) infoEl.dataset.itemId = localId;
const timeEl = document.querySelector('time.timeago');
if (timeEl) {
const nowIso = new Date().toISOString();
timeEl.dataset.iso = nowIso;
const fullDate = typeof window.f0ckFormatDateFull === 'function' ? window.f0ckFormatDateFull(nowIso) : new Date().toLocaleString();
timeEl.setAttribute('tooltip', fullDate);
timeEl.textContent = (window.f0ckTimeAgo ? window.f0ckTimeAgo(nowIso) : (window.f0ckI18n?.timeago_just_now || 'just now'));
}
if (chanRehostBtn) {
chanRehostBtn.classList.add('rehosted');
chanRehostBtn.outerHTML = `
<span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;">
<a href="/${localId}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #${localId})" style="display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;"><i class="fa-solid fa-check"></i> #${localId}</a>
<button type="button" class="chan-rehost-edit-btn" data-item-id="${localId}" title="Edit rating" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"><i class="fa-solid fa-pen"></i></button>
</span>
`;
}
} else {
throw new Error(rehostData.msg || 'Rehost failed');
}
}
const effectivePostId = localId ? Number(localId) : (postid || getContext()?.postid);
if (!effectivePostId) {
throw new Error('Missing post ID');
}
if (mySeq !== favSeq) return;
const res = await post('/api/v2/togglefav', {
postid: effectivePostId,
chan_url: chanUrl,
action: isNowFav ? 'add' : 'delete',
favorited: isNowFav
});
if (mySeq !== favSeq) return;
if (res && res.success) {
if (window.invalidateItemCache) {
window.invalidateItemCache(effectivePostId);
}
const finalIsFav = (res.favorited !== undefined) ? !!res.favorited : isNowFav;
setFavoUi(finalIsFav);
const curFavContainer = document.querySelector('#favs');
if (curFavContainer && Array.isArray(res.favs)) {
curFavContainer.innerHTML = "";
if (res.favs.length > 0) {
const isAnonymized = !!(window.f0ckSession?.is_anonymized ?? (window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in) ?? (window.f0ckSession?.is_anon && window.f0ckSession?.anon_anonymize));
const fragment = document.createDocumentFragment();
res.favs.forEach(f => {
const isSelf = window.f0ckSession && (
(window.f0ckSession.id && f.user_id && Number(window.f0ckSession.id) === Number(f.user_id)) ||
(window.f0ckSession.user && f.user && window.f0ckSession.user.toLowerCase() === f.user.toLowerCase()) ||
(window.f0ckSession.login && f.login && window.f0ckSession.login.toLowerCase() === f.login.toLowerCase())
);
const isFavAnon = f.is_anon || f.user === 'anonymous' || (typeof f.user === 'string' && f.user.startsWith('anon_'));
if (f.hide_fav_badge && !isSelf) {
const a = document.createElement('a');
a.className = 'ghost-fav';
a.setAttribute('tooltip', '?');
a.setAttribute('flow', 'up');
a.style.cursor = 'default';
const img = document.createElement('img');
img.src = '/s/img/ghost_fav.svg';
img.style.height = "32px";
img.style.width = "32px";
a.appendChild(img);
fragment.appendChild(a);
} else if (isAnonymized || isFavAnon) {
const a = document.createElement('a');
a.className = 'ghost-fav';
if (isSelf) a.dataset.self = 'true';
a.setAttribute('tooltip', 'anonymous');
a.setAttribute('flow', 'up');
a.style.cursor = 'default';
const img = document.createElement('img');
img.src = '/a/default.png';
img.style.height = "32px";
img.style.width = "32px";
a.appendChild(img);
fragment.appendChild(a);
} else {
const a = document.createElement('a');
if (isSelf) a.dataset.self = 'true';
a.href = `/user/${(f.user || '').toLowerCase()}`;
a.setAttribute('tooltip', f.display_name || f.user || 'anonymous');
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);
fragment.appendChild(a);
}
});
curFavContainer.appendChild(fragment);
curFavContainer.hidden = false;
} else {
curFavContainer.hidden = true;
}
}
} else {
throw new Error(rehostData.msg || 'Rehost failed');
}
} catch (err) {
console.error('[CHAN-FAV-ADMIN] Auto-rehost error:', err);
setFavoUi(wasAlreadyFav);
if (typeof window.flashMessage === 'function') {
window.flashMessage('Failed to favorite post', 3000, 'error');
}
return;
}
}
const ctx = getContext();
if (!ctx) {
setFavoUi(wasAlreadyFav);
return;
}
const postid = localId ? Number(localId) : ctx.postid;
try {
const res = await post('/api/v2/togglefav', {
postid: postid,
chan_url: chanUrl
});
if (res && res.success) {
if (window.invalidateItemCache) {
window.invalidateItemCache(postid);
}
// Authoritative server state takes priority; fallback to inverting previous UI state
const isNowFav = (res.favorited !== undefined) ? !!res.favorited : !wasAlreadyFav;
setFavoUi(isNowFav);
const favcontainer = document.querySelector('#favs');
if (favcontainer) {
favcontainer.innerHTML = "";
if (res.favs && res.favs.length > 0) {
const isAnonymized = !!(window.f0ckSession?.is_anonymized ?? (window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in) ?? (window.f0ckSession?.is_anon && window.f0ckSession?.anon_anonymize));
const fragment = document.createDocumentFragment();
res.favs.forEach(f => {
const isSelf = window.f0ckSession && (
(window.f0ckSession.id && f.user_id && Number(window.f0ckSession.id) === Number(f.user_id)) ||
(window.f0ckSession.user && f.user && window.f0ckSession.user.toLowerCase() === f.user.toLowerCase()) ||
(window.f0ckSession.login && f.login && window.f0ckSession.login.toLowerCase() === f.login.toLowerCase())
);
const isFavAnon = f.is_anon || f.user === 'anonymous' || (typeof f.user === 'string' && f.user.startsWith('anon_'));
if (f.hide_fav_badge && !isSelf) {
const a = document.createElement('a');
a.className = 'ghost-fav';
a.setAttribute('tooltip', '?');
a.setAttribute('flow', 'up');
a.style.cursor = 'default';
const img = document.createElement('img');
img.src = '/s/img/ghost_fav.svg';
img.style.height = "32px";
img.style.width = "32px";
a.appendChild(img);
fragment.appendChild(a);
} else if (isAnonymized || isFavAnon) {
const a = document.createElement('a');
a.className = 'ghost-fav';
a.setAttribute('tooltip', 'anonymous');
a.setAttribute('flow', 'up');
a.style.cursor = 'default';
const img = document.createElement('img');
img.src = '/a/default.png';
img.style.height = "32px";
img.style.width = "32px";
a.appendChild(img);
fragment.appendChild(a);
} else {
const a = document.createElement('a');
a.href = `/user/${(f.user || '').toLowerCase()}`;
a.setAttribute('tooltip', f.display_name || f.user || 'anonymous');
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);
fragment.appendChild(a);
}
});
favcontainer.appendChild(fragment);
favcontainer.hidden = false;
} else {
favcontainer.hidden = true;
if (mySeq !== favSeq) return;
setFavoUi(wasAlreadyFav);
const curFavContainer = document.querySelector('#favs');
if (curFavContainer) {
curFavContainer.innerHTML = prevFavsHtml;
curFavContainer.hidden = prevFavsHidden;
}
const errMsg = (res && (res.msg || res.error)) || 'Anonymous favoriting is disabled.';
if (typeof window.flashMessage === 'function') {
window.flashMessage(errMsg, 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 {
} catch (err) {
console.error('[CHAN-FAV-ADMIN] Error during background favorite sync:', err);
if (mySeq !== favSeq) return;
setFavoUi(wasAlreadyFav);
const errMsg = (res && (res.msg || res.error)) || 'Anonymous favoriting is disabled.';
const curFavContainer = document.querySelector('#favs');
if (curFavContainer) {
curFavContainer.innerHTML = prevFavsHtml;
curFavContainer.hidden = prevFavsHidden;
}
if (typeof window.flashMessage === 'function') {
window.flashMessage(errMsg, 3000, 'error');
window.flashMessage('Failed to update favorite.', 3000, 'error');
}
}
} catch (err) {
setFavoUi(wasAlreadyFav);
if (typeof window.flashMessage === 'function') {
window.flashMessage('Failed to update favorite.', 3000, 'error');
}
}
})();
};
const deleteSubItemEvent = async (subf0ck, postid) => {