This commit is contained in:
2026-09-21 00:02:48 +02:00
parent 4e857fd5ef
commit 990f463534
17 changed files with 690 additions and 168 deletions
+47
View File
@@ -7331,6 +7331,53 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
text-decoration: underline;
}
.chan-rehost-action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 4px 16px;
min-width: 100px;
box-sizing: border-box;
white-space: nowrap;
flex-shrink: 0;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 600;
font-family: inherit;
background: rgba(74, 222, 128, 0.15);
color: #4ade80;
border: 1px solid rgba(74, 222, 128, 0.3);
cursor: pointer;
text-decoration: none;
transition: all 0.15s ease;
}
.chan-rehost-action-btn:hover {
background: rgba(74, 222, 128, 0.28);
border-color: rgba(74, 222, 128, 0.55);
color: #4ade80;
text-decoration: none;
}
.chan-rehost-action-btn:active {
transform: scale(0.98);
}
.chan-rehost-action-btn.rehosted {
background: rgba(74, 222, 128, 0.2);
border-color: rgba(74, 222, 128, 0.4);
color: #4ade80;
min-width: auto;
padding: 4px 12px;
}
.chan-rehost-action-btn.loading {
opacity: 0.75;
cursor: wait;
pointer-events: none;
}
.item_title,
.item-title,
.item-itle {
+37 -13
View File
@@ -293,17 +293,34 @@
return;
}
const favoBtns = document.querySelectorAll("#a_favo");
if (!favoBtns.length) return;
const firstFavoBtn = favoBtns[0];
const chanRehostBtn = document.querySelector('#chan-item-rehost-btn');
const isChan = firstFavoBtn?.dataset?.isChan === 'true' || !!chanRehostBtn;
const chanUrl = firstFavoBtn?.dataset?.chanUrl || chanRehostBtn?.dataset?.url;
let localId = firstFavoBtn?.dataset?.localId;
const wasAlreadyFav = favoBtns[0].classList.contains('fa-solid') && !favoBtns[0].classList.contains('fa-spinner');
const setFavoUi = (isFav) => {
favoBtns.forEach(btn => {
btn.classList.remove('fa-spinner', 'fa-spin');
btn.classList.add('iconset', 'fa-heart');
btn.classList.toggle('fa-solid', isFav);
btn.classList.toggle('fa-regular', !isFav);
});
};
const setFavoLoading = () => {
favoBtns.forEach(btn => {
btn.classList.remove('fa-heart');
btn.classList.add('iconset', 'fa-solid', 'fa-spinner', 'fa-spin');
});
};
// If viewing un-rehosted 4chan post, auto-rehost first
if (isChan && !localId && chanUrl) {
favoBtns.forEach(btn => {
btn.className = 'iconset fa-solid fa-spinner fa-spin';
});
setFavoLoading();
try {
const csrfToken = window.f0ckSession?.csrf_token || '';
const rehostResp = await fetch('/api/v2/scroller/rehost', {
@@ -331,6 +348,15 @@
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 = `
@@ -345,9 +371,7 @@
}
} catch (err) {
console.error('[CHAN-FAV-ADMIN] Auto-rehost error:', err);
favoBtns.forEach(btn => {
btn.className = 'iconset fa-regular fa-heart';
});
setFavoUi(wasAlreadyFav);
if (typeof window.flashMessage === 'function') {
window.flashMessage('Failed to favorite post', 3000, 'error');
}
@@ -356,11 +380,12 @@
}
const ctx = getContext();
if (!ctx) return;
if (!ctx) {
setFavoUi(wasAlreadyFav);
return;
}
const postid = localId ? Number(localId) : ctx.postid;
const wasAlreadyFav = favoBtns.length > 0 && favoBtns[0].classList.contains('fa-solid');
try {
const res = await post('/api/v2/togglefav', {
postid: postid,
@@ -373,10 +398,7 @@
// Authoritative server state takes priority; fallback to inverting previous UI state
const isNowFav = (res.favorited !== undefined) ? !!res.favorited : !wasAlreadyFav;
favoBtns.forEach(favoBtn => {
favoBtn.classList.toggle('fa-solid', isNowFav);
favoBtn.classList.toggle('fa-regular', !isNowFav);
});
setFavoUi(isNowFav);
const favcontainer = document.querySelector('#favs');
if (favcontainer) {
@@ -446,12 +468,14 @@
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 {
setFavoUi(wasAlreadyFav);
const errMsg = (res && (res.msg || res.error)) || 'Anonymous favoriting is disabled.';
if (typeof window.flashMessage === 'function') {
window.flashMessage(errMsg, 3000, 'error');
}
}
} catch (err) {
setFavoUi(wasAlreadyFav);
if (typeof window.flashMessage === 'function') {
window.flashMessage('Failed to update favorite.', 3000, 'error');
}
+423 -62
View File
@@ -3476,7 +3476,15 @@ window.cancelAnimFrame = (function () {
}
const locEl = document.querySelector('.location');
if (locEl && !locEl.textContent.includes('#')) {
locEl.textContent = locEl.textContent.replace(/#.*$/, '') + '#' + initialHash;
const lastLink = locEl.querySelector('a:last-of-type');
if (lastLink) {
lastLink.textContent = lastLink.textContent.replace(/#.*$/, '') + '#' + initialHash;
if (lastLink.href) {
lastLink.href = lastLink.href.replace(/#.*$/, '') + '#' + initialHash;
}
} else {
locEl.textContent = locEl.textContent.replace(/#.*$/, '') + '#' + initialHash;
}
}
const foundIdx = albumData.findIndex(item =>
String(item.slug || '') === initialHash ||
@@ -3600,7 +3608,15 @@ window.cancelAnimFrame = (function () {
}
const locEl = document.querySelector('.location');
if (locEl && subKey) {
locEl.textContent = locEl.textContent.replace(/#.*$/, '') + '#' + subKey;
const lastLink = locEl.querySelector('a:last-of-type');
if (lastLink) {
lastLink.textContent = lastLink.textContent.replace(/#.*$/, '') + '#' + subKey;
if (lastLink.href) {
lastLink.href = lastLink.href.replace(/#.*$/, '') + '#' + subKey;
}
} else {
locEl.textContent = locEl.textContent.replace(/#.*$/, '') + '#' + subKey;
}
}
// Pause any playing audio or video
@@ -11023,6 +11039,50 @@ window.cancelAnimFrame = (function () {
if (_navEl && window.ResizeObserver) {
new ResizeObserver(window.syncNavbarHeight).observe(_navEl);
}
const _existingNavbarLogo = document.getElementById('navbar-logo');
if (_existingNavbarLogo && !_existingNavbarLogo.complete) {
_existingNavbarLogo.addEventListener('load', window.syncNavbarHeight);
}
window.updateBrandImage = (url) => {
const logos = document.querySelectorAll('#navbar-logo');
if (url) {
window.f0ckBrandImages = [url];
if (logos.length > 0) {
logos.forEach(el => {
el.src = url;
el.style.display = '';
el.onload = () => { if (window.syncNavbarHeight) window.syncNavbarHeight(); };
});
} else {
document.querySelectorAll('a.navbar-brand').forEach(brandLink => {
brandLink.querySelectorAll('.f0ck').forEach(s => s.style.display = 'none');
const img = document.createElement('img');
img.id = 'navbar-logo';
img.src = url;
img.alt = window.f0ckDomain || document.title;
img.style.cssText = 'max-height:40px;vertical-align:middle;max-width:180px;width:auto;';
img.onload = () => { if (window.syncNavbarHeight) window.syncNavbarHeight(); };
brandLink.prepend(img);
});
}
} else {
window.f0ckBrandImages = [];
logos.forEach(el => el.remove());
document.querySelectorAll('a.navbar-brand').forEach(brandLink => {
const f0ckSpan = brandLink.querySelector('.f0ck');
if (f0ckSpan) {
f0ckSpan.style.display = '';
} else {
const span = document.createElement('span');
span.className = 'f0ck';
span.textContent = window.f0ckCustomBrandText || window.f0ckDomain || '';
brandLink.appendChild(span);
}
});
}
if (window.syncNavbarHeight) window.syncNavbarHeight();
};
// Sidebar state init — apply persisted hidden/visible state on load
window.initSidebarRightToggle = () => {
@@ -11152,7 +11212,10 @@ window.cancelAnimFrame = (function () {
const itemIdMatch = url.match(/\/([a-zA-Z0-9_-]{11}|\d+)(?:\?.*|#.*)?$/);
if (itemIdMatch) {
const itemId = itemIdMatch[1];
document.querySelectorAll(`a.thumb.has-notif[href$="/${itemId}"], a.thumb.has-notif[data-item-id="${itemId}"]`).forEach(el => {
if (window.NotificationSystemInstance) {
window.NotificationSystemInstance.markItemRead(itemId);
}
document.querySelectorAll(`a.thumb.has-notif[href$="/${itemId}"], a.thumb.has-notif[data-item-id="${itemId}"], a.lazy-thumb.has-notif[data-item-id="${itemId}"]`).forEach(el => {
el.classList.remove('has-notif');
});
}
@@ -11265,11 +11328,12 @@ window.cancelAnimFrame = (function () {
// Restore scroll position
requestAnimationFrame(() => {
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
const ic = document.querySelector('.index-container');
if (ic && cache.scroll !== undefined) {
ic.scrollTo(0, cache.scroll);
} else {
window.scrollTo(0, cache.scroll || 0);
}
});
@@ -11283,8 +11347,9 @@ window.cancelAnimFrame = (function () {
window.updateVisitIndicators();
window.initLazyLoading();
if (window.initSidebarRightToggle) window.initSidebarRightToggle();
if (window.syncNavbarHeight) window.syncNavbarHeight();
window.NotificationSystemInstance?.pollDebounced?.();
if (window.NotificationSystemInstance) {
window.NotificationSystemInstance.poll();
}
window._onaraCurrentGridUrl = urlObj.pathname + urlObj.search;
isNavigating = false;
return; // SKIP FETCH
@@ -11342,6 +11407,11 @@ window.cancelAnimFrame = (function () {
indexWrapper.className = 'index-layout-wrapper';
indexWrapper.innerHTML = '<div class="index-container"><div class="posts grid-transition"></div></div>';
// Reset window scroll to 0 before appending indexWrapper so :has(.index-container) doesn't lock a scrolled offset
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
main.innerHTML = '';
main.appendChild(indexWrapper);
@@ -11764,7 +11834,10 @@ window.cancelAnimFrame = (function () {
// SCROLL TO TOP ON EVERY NEW NAVIGATION
if (!options.skipScroll) {
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
}
if (window.syncNavbarHeight) window.syncNavbarHeight();
// Re-init activity feed if applicable
if (document.getElementById('activity-container')) {
@@ -11891,15 +11964,20 @@ window.cancelAnimFrame = (function () {
if (options.skipPush && history.state && history.state.scroll !== undefined) {
// Back/Forward Navigation: Restore saved scroll position
requestAnimationFrame(() => {
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
const ic = document.querySelector('.index-container');
if (ic) ic.scrollTo(0, history.state.scroll);
else window.scrollTo(0, history.state.scroll);
});
} else if (replace) {
// New Navigation: Reset to top
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
const ic = document.querySelector('.index-container');
if (ic) ic.scrollTo(0, 0);
else window.scrollTo(0, 0);
}
// Trigger fade-in
@@ -11981,6 +12059,7 @@ window.cancelAnimFrame = (function () {
// Notify extensions and re-init systems (e.g. CommentSystem)
document.dispatchEvent(new Event('f0ck:contentLoaded'));
if (window.initSidebarRightToggle) window.initSidebarRightToggle();
if (window.syncNavbarHeight) window.syncNavbarHeight();
// Sync has-notif highlights after grid loads — handles PWA where visibilitychange doesn't fire
window.NotificationSystemInstance?.pollDebounced?.();
window._onaraCurrentGridUrl = urlObj.pathname + urlObj.search;
@@ -12237,6 +12316,14 @@ window.cancelAnimFrame = (function () {
return;
}
const itemid = keySegments.pop();
if (itemid) {
if (window.NotificationSystemInstance) {
window.NotificationSystemInstance.markItemRead(itemid);
}
document.querySelectorAll(`a.thumb.has-notif[data-item-id="${itemid}"], a.thumb.has-notif[href$="/${itemid}"], a.lazy-thumb.has-notif[data-item-id="${itemid}"]`).forEach(el => {
el.classList.remove('has-notif');
});
}
// Extract context from Target URL first
let tag = null, user = null, isFavs = false, mime = null, hall = null, userHall = null, userHallOwner = null, tagger = null;
@@ -12427,6 +12514,11 @@ window.cancelAnimFrame = (function () {
const _ic = main.querySelector('.index-container');
const _ntc = _iw || _ic;
if (_ntc && _isIndexPage) {
if (itemid) {
_ntc.querySelectorAll(`a.thumb.has-notif[data-item-id="${itemid}"], a.thumb.has-notif[href$="/${itemid}"], a.lazy-thumb.has-notif[data-item-id="${itemid}"]`).forEach(el => {
el.classList.remove('has-notif');
});
}
const _ck = window.location.pathname + window.location.search;
const _pw = document.querySelector('.pagination-wrapper');
gridCacheMap.set(_ck, { node: _ntc, scroll: currentScroll, pagination: _pw ? _pw.innerHTML : '' });
@@ -12707,6 +12799,11 @@ window.cancelAnimFrame = (function () {
const nodeToCache = indexWrapper || indexContainer;
if (nodeToCache && isIndexPage) {
if (itemid) {
nodeToCache.querySelectorAll(`a.thumb.has-notif[data-item-id="${itemid}"], a.thumb.has-notif[href$="/${itemid}"], a.lazy-thumb.has-notif[data-item-id="${itemid}"]`).forEach(el => {
el.classList.remove('has-notif');
});
}
const cacheKey = window.location.pathname + window.location.search;
const paginationWrapper = document.querySelector('.pagination-wrapper');
gridCacheMap.set(cacheKey, {
@@ -13021,6 +13118,13 @@ window.cancelAnimFrame = (function () {
const thumbnail = target.closest('.posts > a:not(.notif-item)');
if (thumbnail && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
e.preventDefault();
if (thumbnail.classList.contains('has-notif')) {
thumbnail.classList.remove('has-notif');
}
const clickedId = thumbnail.dataset.itemId || thumbnail.getAttribute('data-item-id') || thumbnail.getAttribute('href')?.match(/\/([a-zA-Z0-9_-]{11}|\d+)(?:[?#]|$)/)?.[1];
if (clickedId && window.NotificationSystemInstance) {
window.NotificationSystemInstance.markItemRead(clickedId);
}
if (isOnaraActive()) {
const wasOpen = document.body.classList.contains('onara-modal-open');
document.querySelectorAll('.onara-active').forEach(el => el.classList.remove('onara-active'));
@@ -13459,10 +13563,13 @@ window.cancelAnimFrame = (function () {
const ratingGroup = modal.querySelector('#chan-rehost-rating-group');
let selectedRating = 'untagged';
let userSelectedRating = false;
let userEditedTags = false;
ratingGroup.addEventListener('click', (e) => {
const pill = e.target.closest('.chan-rehost-pill');
if (!pill) return;
userSelectedRating = true;
ratingGroup.querySelectorAll('.chan-rehost-pill').forEach(b => b.classList.remove('active'));
pill.classList.add('active');
selectedRating = pill.dataset.rating;
@@ -13471,19 +13578,23 @@ window.cancelAnimFrame = (function () {
const tagsInput = modal.querySelector('#chan-rehost-tags-input');
const commentInput = modal.querySelector('#chan-rehost-comment-input');
// Pre-fill current rating and tags if available
tagsInput.addEventListener('input', () => {
userEditedTags = true;
});
// Pre-fill current rating and tags if available (don't overwrite if user already interacted)
fetch(`/api/v2/scroller/rehost/details/${itemId}`)
.then(r => r.json())
.then(data => {
if (data && data.success) {
if (data.rating) {
if (data.rating && data.rating !== 'untagged' && !userSelectedRating) {
selectedRating = data.rating;
ratingGroup.querySelectorAll('.chan-rehost-pill').forEach(b => {
b.classList.toggle('active', b.dataset.rating === data.rating);
});
}
if (data.tags && data.tags.length) {
tagsInput.value = data.tags.join(', ');
if (data.tags && data.tags.length && !userEditedTags && !tagsInput.value) {
tagsInput.value = data.tags.map(t => (typeof t === 'string' ? t : t.tag)).join(', ');
}
}
}).catch(() => {});
@@ -13507,6 +13618,122 @@ window.cancelAnimFrame = (function () {
if (e.target === modal) closeModal();
};
const updateDomAfterSave = (resData) => {
try {
const finalRating = resData.rating || selectedRating || 'untagged';
const finalTags = Array.isArray(resData.tags) ? resData.tags : [];
// 1. Update #tags container
const tagsContainer = document.querySelector('#tags');
if (tagsContainer) {
tagsContainer.dataset.itemId = itemId;
tagsContainer.dataset.canManage = 'true';
const ratingBadges = {
sfw: { cls: 'badge-success is-sfw', label: 'SFW' },
nsfw: { cls: 'badge-warning is-nsfw', label: 'NSFW' },
nsfl: { cls: 'badge-danger is-nsfl', label: 'NSFL' },
untagged: { cls: 'badge-untagged is-untagged', label: '?' }
};
const rMeta = ratingBadges[finalRating] || ratingBadges.untagged;
let ratingEl = tagsContainer.querySelector('.rating-tag');
if (!ratingEl) {
ratingEl = document.createElement('span');
tagsContainer.prepend(ratingEl);
}
ratingEl.className = `badge ${rMeta.cls} rating-tag can-cycle`;
ratingEl.dataset.itemId = itemId;
ratingEl.dataset.rating = finalRating;
ratingEl.innerHTML = `<span class="rating-label">${rMeta.label}</span>`;
// Remove previous tag badges and re-render new ones
tagsContainer.querySelectorAll('.tag-badge').forEach(el => el.remove());
finalTags.forEach(t => {
const tagName = typeof t === 'string' ? t : (t.tag || '');
const tagNorm = typeof t === 'string' ? tagName.toLowerCase().replace(/[^a-z0-9_-]/g, '') : (t.normalized || tagName);
const tagBadge = (typeof t === 'object' && t.badge) ? t.badge : 'badge-light';
const tagId = (typeof t === 'object' && t.id) ? t.id : '';
const span = document.createElement('span');
span.className = `badge ${tagBadge} tag-badge`;
if (tagId) span.dataset.tagId = tagId;
span.dataset.tag = tagName;
span.dataset.tagNormalized = tagNorm;
const a = document.createElement('a');
a.href = `/tag/${encodeURIComponent(tagNorm)}`;
a.className = 'tag-name';
a.textContent = tagName;
span.appendChild(a);
tagsContainer.appendChild(span);
});
}
// 2. Update .item-layout-container
const layoutContainer = document.querySelector('.item-layout-container');
if (layoutContainer) {
layoutContainer.dataset.itemId = itemId;
}
// 3. Update .media-object mode
const mediaObj = document.querySelector('.media-object');
if (mediaObj) {
mediaObj.dataset.mode = finalRating;
}
// 4. Update .location breadcrumb
const locEl = document.querySelector('.location');
if (locEl && !locEl.querySelector('.is-rehosted')) {
const sep = document.createElement('span');
sep.className = 'location-sep';
sep.style.cssText = 'opacity: 0.6; margin: 0 4px;';
sep.textContent = '→';
const a = document.createElement('a');
a.href = `/${itemId}`;
a.className = 'location-link is-rehosted';
a.title = `View rehosted post #${itemId} on f0ckm`;
a.style.cssText = 'color: #4ade80; font-weight: 700;';
a.innerHTML = `<i class="fa-solid fa-check"></i> #${itemId}`;
locEl.appendChild(sep);
locEl.appendChild(a);
}
// 5. Update #a_favo
const favBtn = document.querySelector('#a_favo');
if (favBtn) {
favBtn.dataset.itemId = itemId;
favBtn.dataset.localId = itemId;
}
// 6. Update #chan-item-rehost-btn if present
const rehostBtn = document.querySelector('#chan-item-rehost-btn');
if (rehostBtn) {
rehostBtn.outerHTML = `
<span class="chan-rehost-btn-group" style="display:inline-flex;gap:4px;align-items:center;">
<a href="/${itemId}" class="chan-rehost-action-btn rehosted" title="View Post #${itemId}" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;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;white-space:nowrap;flex-shrink:0;">
<i class="fa-solid fa-check"></i> #${itemId}
</a>
<button type="button" class="chan-rehost-edit-btn" data-item-id="${itemId}" 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>
`;
}
// 7. Invalidate caches
if (typeof window.invalidateItemCache === 'function') {
window.invalidateItemCache(itemId);
window.invalidateItemCache(window.location.pathname);
}
} catch (err) {
console.error('[REHOST-MODAL] DOM update error:', err);
}
};
const saveDetails = async (andView = false) => {
const saveBtn = modal.querySelector('.chan-rehost-modal-save');
const saveViewBtn = modal.querySelector('.chan-rehost-modal-save-view');
@@ -13535,12 +13762,13 @@ window.cancelAnimFrame = (function () {
window.showToast('Details saved for #' + itemId + '!');
}
closeModal();
updateDomAfterSave(resData);
if (typeof onSave === 'function') {
onSave(resData);
}
if (andView) {
if (typeof window.loadItemAjax === 'function') {
window.loadItemAjax('/' + itemId);
window.loadItemAjax('/' + itemId, true, { skipCache: true });
} else {
window.location.href = '/' + itemId;
}
@@ -13602,9 +13830,19 @@ window.cancelAnimFrame = (function () {
if (data.success && data.item_id) {
btn.classList.remove('loading');
btn.classList.add('rehosted');
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'));
}
btn.outerHTML = `
<span class="chan-rehost-btn-group" style="display:inline-flex;gap:4px;align-items:center;">
<a href="/${data.item_id}" class="chan-rehost-action-btn rehosted" title="View Post #${data.item_id}" 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;">
<a href="/${data.item_id}" class="chan-rehost-action-btn rehosted" title="View Post #${data.item_id}" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;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;white-space:nowrap;flex-shrink:0;">
<i class="fa-solid fa-check"></i> #${data.item_id}
</a>
<button type="button" class="chan-rehost-edit-btn" data-item-id="${data.item_id}" 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;">
@@ -14146,6 +14384,10 @@ window.cancelAnimFrame = (function () {
loadPageAjax(url, true, { skipPush: true });
} else {
document.title = window._onaraReturnTitle || window.f0ckDomain || 'f0ck';
if (window.updateVisitIndicators) window.updateVisitIndicators();
if (window.NotificationSystemInstance) {
window.NotificationSystemInstance.poll();
}
}
}
return;
@@ -17723,31 +17965,9 @@ class NotificationSystem {
document.dispatchEvent(new CustomEvent('f0ck:global_chat_presence', { detail: data.data }));
} else if (data.type === 'brand_image') {
window.f0ckDebug(`[SSE] Brand image update received:`, data.data?.url);
if (data.data?.url) {
const src = data.data.url;
// Update the randomizeLogo pool so clicking the brand always uses the current image
window.f0ckBrandImages = [src];
const logos = document.querySelectorAll('#navbar-logo');
if (logos.length > 0) {
logos.forEach(el => { el.src = src; el.style.display = ''; });
} else {
// Logo element doesn't exist yet (no image was set before) — create it
document.querySelectorAll('a.navbar-brand').forEach(brandLink => {
const textNode = Array.from(brandLink.childNodes).find(n => n.nodeType === Node.TEXT_NODE);
const img = document.createElement('img');
img.id = 'navbar-logo';
img.src = src;
img.alt = document.title;
img.style.cssText = 'max-height:40px;vertical-align:middle;max-width:180px;width:auto;';
if (textNode) brandLink.insertBefore(img, textNode);
else brandLink.prepend(img);
});
}
} else {
window.f0ckBrandImages = [];
document.querySelectorAll('#navbar-logo').forEach(el => el.remove());
if (window.updateBrandImage) {
window.updateBrandImage(data.data?.url || null);
}
}
} catch (err) {
console.error('SSE data parse error', err);
@@ -18053,7 +18273,12 @@ class NotificationSystem {
};
document.addEventListener('f0ck:contentLoaded', reapplyOwnBanner);
window.addEventListener('pageshow', (e) => { if (e.persisted) reapplyOwnBanner(); });
window.addEventListener('pageshow', (e) => {
if (e.persisted) {
reapplyOwnBanner();
this.poll();
}
});
// Poll fresh display_name from DB — used on tab focus to catch missed SSE events
this.syncDisplayName = async () => {
@@ -18218,6 +18443,12 @@ class NotificationSystem {
// Handle "Mark as Read"
const notifTargetItemId = link.dataset.itemId || link.getAttribute('data-item-id') || link.getAttribute('href')?.match(/\/([a-zA-Z0-9_-]{11}|\d+)(?:[?#]|$)/)?.[1];
const notifTargetItemSlug = link.dataset.itemSlug || link.getAttribute('data-item-slug');
if (notifTargetItemId || notifTargetItemSlug) {
this.markItemRead(notifTargetItemId, notifTargetItemSlug);
}
if (link.dataset.id && link.classList.contains('unread')) {
window.f0ckDebug(`[NotificationSystem] Marking ${link.dataset.id} as read...`);
// Fire and forget (keepalive ensures it survives navigation)
@@ -18226,8 +18457,8 @@ class NotificationSystem {
keepalive: true
}).catch(err => console.error("Failed to mark read", err));
// Decrement badge count
if (this.countBadge) {
// Decrement badge count if not already decremented by markItemRead
if (!notifTargetItemId && !notifTargetItemSlug && this.countBadge) {
let count = parseInt(this.countBadge.textContent) || 0;
if (count > 0) {
count--;
@@ -18521,21 +18752,32 @@ class NotificationSystem {
const unreadItemIds = new Set();
notifications.forEach(n => {
if (!n.is_read && n.item_id) {
unreadItemIds.add(String(n.item_id));
if (currentPath === `/${n.item_id}` || currentPath === `/${n.item_id}/`) return;
document.querySelectorAll(`a.thumb[href$="/${n.item_id}"], a.lazy-thumb[href$="/${n.item_id}"]`).forEach(el => {
const idStr = String(n.item_id);
unreadItemIds.add(idStr);
if (n.item_slug) unreadItemIds.add(String(n.item_slug));
if (n.slug) unreadItemIds.add(String(n.slug));
if (currentPath === `/${n.item_id}` || currentPath === `/${n.item_id}/` || (n.item_slug && currentPath === `/${n.item_slug}`)) return;
document.querySelectorAll(`a.thumb[data-item-id="${idStr}"], a.lazy-thumb[data-item-id="${idStr}"], a.thumb[href$="/${idStr}"], a.lazy-thumb[href$="/${idStr}"]`).forEach(el => {
el.classList.add('has-notif');
});
}
});
// Remove .has-notif from any thumb whose item is no longer in the unread set.
document.querySelectorAll('a.thumb.has-notif, a.lazy-thumb.has-notif').forEach(el => {
const match = el.getAttribute('href')?.match(/\/(\d+)$/);
if (match && !unreadItemIds.has(match[1])) {
el.classList.remove('has-notif');
}
});
const removeStaleHasNotif = (root = document) => {
root.querySelectorAll('a.thumb.has-notif, a.lazy-thumb.has-notif').forEach(el => {
const id = el.dataset.itemId || el.getAttribute('data-item-id') || el.getAttribute('href')?.match(/\/([a-zA-Z0-9_-]{11}|\d+)(?:[?#]|$)/)?.[1];
if (id && !unreadItemIds.has(String(id))) {
el.classList.remove('has-notif');
}
});
};
removeStaleHasNotif(document);
if (typeof gridCacheMap !== 'undefined' && gridCacheMap) {
gridCacheMap.forEach(cacheEntry => {
if (cacheEntry?.node) removeStaleHasNotif(cacheEntry.node);
});
}
// Render the active tab
this._renderActiveTab();
@@ -18592,7 +18834,7 @@ class NotificationSystem {
const userLabel = isDeleted ? (i18n.notif_moderation || 'Moderation') : (i18n.notif_system || 'System');
const itemLink = `/${itemKey}`;
return `
<a href="${itemLink}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
<a href="${itemLink}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}">
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/mod/deleted/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';}"></div>
<div class="notif-content">
<div class="notif-user"><strong>${userLabel}</strong></div>
@@ -18644,7 +18886,7 @@ class NotificationSystem {
let thumbSrc, thumbOnerror;
if (n.type === 'warning') {
return `
<div class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
<div class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}">
<div class="notif-thumb" style="display:flex;align-items:center;justify-content:center;background:var(--bg-lighter);color:var(--danger);font-size:1.5em;"><i class="fa-solid fa-triangle-exclamation"></i></div>
<div class="notif-content">
<div class="notif-user"><strong ${n.username_color ? `style="color: ${n.username_color}"` : ''}>${user}</strong></div>
@@ -18662,7 +18904,7 @@ class NotificationSystem {
}
const thumb = n.item_id ? `<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="${thumbSrc}" alt="thumb" onerror="${thumbOnerror}"></div>` : '';
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}">
${thumb}
<div class="notif-content">
<div class="notif-user"><strong ${n.username_color ? `style="color: ${n.username_color}"` : ''}>${user}</strong></div>
@@ -18678,7 +18920,7 @@ class NotificationSystem {
if (n.type === 'approve') {
const link = `/${itemKey}`;
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}">
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/${n.item_id}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';};}"></div>
<div class="notif-content">
<div>
@@ -18693,7 +18935,7 @@ class NotificationSystem {
if (n.type === 'upload_success') {
const link = `/${itemKey}`;
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}">
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.style.display='none';"></div>
<div class="notif-content">
<div>
@@ -18712,7 +18954,7 @@ class NotificationSystem {
const urlDisplay = url ? `<div style="font-size: 0.8em; opacity: 0.7; margin-top: 4px; word-break: break-all; max-height: 3.2em; overflow: hidden;">${url}</div>` : '';
const link = n.item_id ? `/${itemKey}` : '#';
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'}" data-id="${n.id}">
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'}" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}">
<div class="notif-content">
<div>
<strong>${(window.f0ckI18n && window.f0ckI18n.notif_upload_error) || 'Your background upload failed.'}</strong>
@@ -18730,7 +18972,7 @@ class NotificationSystem {
const label = isDeleted ? ((window.f0ckI18n && window.f0ckI18n.notif_upload_deleted) || 'A moderator deleted your upload') : ((window.f0ckI18n && window.f0ckI18n.notif_upload_denied) || 'Your upload was denied');
const link = `/notifications#notif-${n.id}`;
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-notif-nav="true">
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}" data-notif-nav="true">
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/mod/deleted/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';}"></div>
<div class="notif-content">
<div>
@@ -18746,7 +18988,7 @@ class NotificationSystem {
if (n.type === 'admin_pending') {
const link = '/mod/approve';
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}">
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/mod/pending/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';}"></div>
<div class="notif-content">
<div>
@@ -18761,7 +19003,7 @@ class NotificationSystem {
if (n.type === 'report') {
const link = '/mod/reports';
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}">
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/${n.item_id}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';};}"></div>
<div class="notif-content">
<div>
@@ -18776,7 +19018,7 @@ class NotificationSystem {
if (n.type === 'warning') {
const link = `/notifications?tab=system#notif-${n.id}`;
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-notif-nav="true">
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}" data-notif-nav="true">
<div class="notif-thumb" style="display:flex;align-items:center;justify-content:center;background:var(--bg-lighter);color:var(--danger);font-size:1.5em;"><i class="fa-solid fa-triangle-exclamation"></i></div>
<div class="notif-content">
<div>
@@ -18800,7 +19042,7 @@ class NotificationSystem {
const thumb = n.item_id ? `<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.style.display='none'"></div>` : '';
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}">
${thumb}
<div class="notif-content">
<div>
@@ -18812,6 +19054,113 @@ class NotificationSystem {
`;
}
markItemRead(itemId, slug = null) {
if (!itemId && !slug) return;
const strId = itemId ? String(itemId) : null;
const strSlug = slug ? String(slug) : null;
const matchesItem = (n) => {
if (!n) return false;
if (strId && String(n.item_id) === strId) return true;
if (strSlug && (String(n.item_slug) === strSlug || String(n.slug) === strSlug)) return true;
return false;
};
[this._cachedUser, this._cachedSystem].forEach(list => {
if (!Array.isArray(list)) return;
list.forEach(n => {
if (matchesItem(n) && !n.is_read) {
n.is_read = true;
}
});
});
// Remove matching .notif-item elements from dropdown
if (this.list) {
const items = this.list.querySelectorAll('.notif-item');
items.forEach(el => {
const notifItemId = el.dataset.itemId;
const notifItemSlug = el.dataset.itemSlug;
const notifHref = el.getAttribute('href') || '';
const isMatch = (strId && notifItemId === strId) ||
(strSlug && notifItemSlug === strSlug) ||
(strId && (notifHref === `/${strId}` || notifHref.startsWith(`/${strId}#`) || notifHref.startsWith(`/${strId}?`))) ||
(strSlug && (notifHref === `/${strSlug}` || notifHref.startsWith(`/${strSlug}#`) || notifHref.startsWith(`/${strSlug}?`)));
if (isMatch) {
el.remove();
}
});
if (this.list.children.length === 0) {
this.list.innerHTML = `<div class="notif-empty">${window.f0ckI18n?.no_notifications || 'No new notifications'}</div>`;
}
}
// Mark as read in full history page if open
document.querySelectorAll('.notifications-list-full .notif-item.unread').forEach(el => {
const notifItemId = el.dataset.itemId;
const notifItemSlug = el.dataset.itemSlug;
const notifHref = el.getAttribute('href') || '';
const isMatch = (strId && notifItemId === strId) ||
(strSlug && notifItemSlug === strSlug) ||
(strId && (notifHref === `/${strId}` || notifHref.startsWith(`/${strId}#`) || notifHref.startsWith(`/${strId}?`))) ||
(strSlug && (notifHref === `/${strSlug}` || notifHref.startsWith(`/${strSlug}#`) || notifHref.startsWith(`/${strSlug}?`)));
if (isMatch) {
el.classList.remove('unread');
}
});
// Recalculate unread counts
const userUnread = (this._cachedUser || []).filter(n => !n.is_read).length;
const systemUnread = (this._cachedSystem || []).filter(n => !n.is_read).length;
const totalUnread = userUnread + systemUnread;
if (this.countBadge) {
this.countBadge.textContent = totalUnread;
this.countBadge.style.display = totalUnread > 0 ? 'block' : 'none';
}
const userBadge = document.getElementById('notif-tab-badge-user');
const systemBadge = document.getElementById('notif-tab-badge-system');
if (userBadge) {
userBadge.textContent = userUnread;
userBadge.style.display = userUnread > 0 ? '' : 'none';
}
if (systemBadge) {
systemBadge.textContent = systemUnread;
systemBadge.style.display = systemUnread > 0 ? '' : 'none';
}
if (typeof window._scrollerNotifHook === 'function') {
window._scrollerNotifHook(totalUnread);
}
// Remove .has-notif from matching thumbnails in DOM
const removeHasNotif = (root = document) => {
root.querySelectorAll('a.thumb.has-notif, a.lazy-thumb.has-notif').forEach(el => {
const id = el.dataset.itemId || el.getAttribute('data-item-id') || el.getAttribute('href')?.match(/\/([a-zA-Z0-9_-]{11}|\d+)(?:[?#]|$)/)?.[1];
if (id && (id === strId || id === strSlug)) {
el.classList.remove('has-notif');
}
});
};
removeHasNotif(document);
// Also strip from any detached cached grids in gridCacheMap
if (typeof gridCacheMap !== 'undefined' && gridCacheMap) {
gridCacheMap.forEach(cacheEntry => {
if (cacheEntry && cacheEntry.node) {
removeHasNotif(cacheEntry.node);
}
});
}
// Tell server in background (fire-and-forget with keepalive)
const serverId = strId || strSlug;
if (serverId) {
fetch(`/api/notifications/item/${encodeURIComponent(serverId)}/read`, {
method: 'POST',
keepalive: true
}).catch(() => {});
}
}
async markAllRead() {
try {
@@ -18829,7 +19178,7 @@ class NotificationSystem {
}
markAllReadUI() {
this.countBadge.style.display = 'none';
if (this.countBadge) this.countBadge.style.display = 'none';
// Clear per-tab badges
const userBadge = document.getElementById('notif-tab-badge-user');
const systemBadge = document.getElementById('notif-tab-badge-system');
@@ -18843,6 +19192,16 @@ class NotificationSystem {
}
// Also update History page items if present
document.querySelectorAll('.notifications-list-full .notif-item.unread').forEach(el => el.classList.remove('unread'));
// Remove .has-notif highlights
document.querySelectorAll('a.thumb.has-notif, a.lazy-thumb.has-notif').forEach(el => el.classList.remove('has-notif'));
if (typeof gridCacheMap !== 'undefined' && gridCacheMap) {
gridCacheMap.forEach(cacheEntry => {
if (cacheEntry?.node) cacheEntry.node.querySelectorAll('a.thumb.has-notif, a.lazy-thumb.has-notif').forEach(el => el.classList.remove('has-notif'));
});
}
if (typeof window._scrollerNotifHook === 'function') {
window._scrollerNotifHook(0);
}
}
handleTagsUpdate(data) {
@@ -19053,6 +19412,8 @@ class NotificationSystem {
return false;
});
favoBtns.forEach(favoBtn => {
favoBtn.classList.remove('fa-spinner', 'fa-spin');
favoBtn.classList.add('iconset', 'fa-heart');
favoBtn.classList.toggle('fa-solid', isNowFav);
favoBtn.classList.toggle('fa-regular', !isNowFav);
});
+37 -13
View File
@@ -257,17 +257,34 @@
return;
}
const favoBtns = document.querySelectorAll("#a_favo");
if (!favoBtns.length) return;
const firstFavoBtn = favoBtns[0];
const chanRehostBtn = document.querySelector('#chan-item-rehost-btn');
const isChan = firstFavoBtn?.dataset?.isChan === 'true' || !!chanRehostBtn;
const chanUrl = firstFavoBtn?.dataset?.chanUrl || chanRehostBtn?.dataset?.url;
let localId = firstFavoBtn?.dataset?.localId;
const wasAlreadyFav = favoBtns[0].classList.contains('fa-solid') && !favoBtns[0].classList.contains('fa-spinner');
const setFavoUi = (isFav) => {
favoBtns.forEach(btn => {
btn.classList.remove('fa-spinner', 'fa-spin');
btn.classList.add('iconset', 'fa-heart');
btn.classList.toggle('fa-solid', isFav);
btn.classList.toggle('fa-regular', !isFav);
});
};
const setFavoLoading = () => {
favoBtns.forEach(btn => {
btn.classList.remove('fa-heart');
btn.classList.add('iconset', 'fa-solid', 'fa-spinner', 'fa-spin');
});
};
// If viewing un-rehosted 4chan post, auto-rehost first
if (isChan && !localId && chanUrl) {
favoBtns.forEach(btn => {
btn.className = 'iconset fa-solid fa-spinner fa-spin';
});
setFavoLoading();
try {
const csrfToken = window.f0ckSession?.csrf_token || '';
const rehostResp = await fetch('/api/v2/scroller/rehost', {
@@ -295,6 +312,15 @@
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 = `
@@ -309,9 +335,7 @@
}
} catch (err) {
console.error('[CHAN-FAV] Auto-rehost error:', err);
favoBtns.forEach(btn => {
btn.className = 'iconset fa-regular fa-heart';
});
setFavoUi(wasAlreadyFav);
if (typeof window.flashMessage === 'function') {
window.flashMessage('Failed to favorite post', 3000, 'error');
}
@@ -320,11 +344,12 @@
}
const ctx = getContext();
if (!ctx) return;
if (!ctx) {
setFavoUi(wasAlreadyFav);
return;
}
const postid = localId ? Number(localId) : ctx.postid;
const wasAlreadyFav = favoBtns.length > 0 && favoBtns[0].classList.contains('fa-solid');
try {
const res = await post('/api/v2/togglefav', {
postid: postid,
@@ -337,10 +362,7 @@
// Authoritative server state takes priority; fallback to inverting previous UI state
const isNowFav = (res.favorited !== undefined) ? !!res.favorited : !wasAlreadyFav;
favoBtns.forEach(favoBtn => {
favoBtn.classList.toggle('fa-solid', isNowFav);
favoBtn.classList.toggle('fa-regular', !isNowFav);
});
setFavoUi(isNowFav);
// span#favs
const favcontainer = document.querySelector('#favs');
@@ -412,12 +434,14 @@
if (navigator.vibrate) navigator.vibrate(50);
}
else {
setFavoUi(wasAlreadyFav);
const errMsg = (res && (res.msg || res.error)) || 'Anonymous favoriting is disabled.';
if (typeof window.flashMessage === 'function') {
window.flashMessage(errMsg, 3000, 'error');
}
}
} catch (err) {
setFavoUi(wasAlreadyFav);
if (typeof window.flashMessage === 'function') {
window.flashMessage('Failed to update favorite.', 3000, 'error');
}