fix rating and fav

This commit is contained in:
2026-08-13 21:14:20 +02:00
parent 402a043b57
commit 9ec07e8e6c
9 changed files with 53 additions and 17 deletions

View File

@@ -186,6 +186,9 @@
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;

View File

@@ -4145,6 +4145,7 @@ window.cancelAnimFrame = (function () {
.then(r => r.json())
.then(data => {
if (data.success) {
if (window.invalidateItemCache) window.invalidateItemCache(id);
const newOc = data.is_oc;
ocBtn.dataset.isOc = newOc;
ocBtn.setAttribute('title', newOc ? 'Remove OC status' : 'Mark as OC');
@@ -4486,7 +4487,13 @@ window.cancelAnimFrame = (function () {
.then(r => r.json())
.then(res => {
toggleBtn._activeRequestsCount = Math.max(0, (toggleBtn._activeRequestsCount || 1) - 1);
// Evict cached item HTML immediately on success so navigating back fetches fresh content,
// even if the user already navigated to a different item while this request was in flight.
if (res && res.success && window.invalidateItemCache) {
window.invalidateItemCache(postid);
}
// Verify we are still on the same post (prevent dynamic/PJAX page leaks)
const currentIdStr = window.getCurrentItemId();
const currentPostId = currentIdStr ? parseInt(currentIdStr, 10) : null;
@@ -4502,10 +4509,6 @@ window.cancelAnimFrame = (function () {
if (window.renderTags) {
window.renderTags(res.tags);
}
// Evict the cached item HTML so navigating back fetches fresh content
if (window.invalidateItemCache) {
window.invalidateItemCache(postid);
}
} else {
revert();
window.flashMessage('Error: ' + (res.msg || 'Failed to update rating'), 3000, 'error');
@@ -7148,14 +7151,21 @@ window.cancelAnimFrame = (function () {
window.loadPageAjax = loadPageAjax;
window.loadItemAjax = loadItemAjax;
// Invalidate all itemCacheMap entries that contain a given item ID.
// Call this after mutating an item (e.g. adding a tag) so the next
// Invalidate all itemCacheMap entries that match a given item ID or slug.
// Call this after mutating an item (e.g. rating, favorite, tags, OC) so the next
// AJAX navigation fetches fresh HTML instead of serving stale cache.
window.invalidateItemCache = (itemId) => {
if (!itemId) return;
const needle = `/${itemId}`;
for (const key of itemCacheMap.keys()) {
if (key.includes(needle)) {
if (itemId === undefined || itemId === null || itemId === '') return;
const strId = String(itemId).trim();
for (const [key, val] of itemCacheMap.entries()) {
const urlPath = key.split('?')[0];
const segments = urlPath.split('/').filter(Boolean);
const lastSegment = segments[segments.length - 1];
if (lastSegment === strId) {
itemCacheMap.delete(key);
continue;
}
if (val && typeof val.html === 'string' && val.html.includes(`data-item-id="${strId}"`)) {
itemCacheMap.delete(key);
}
}
@@ -8697,6 +8707,10 @@ class NotificationSystem {
return;
}
if (window.invalidateItemCache) {
window.invalidateItemCache(data.item_id);
}
// Check if we are currently viewing this item
const currentIdStr = window.getCurrentItemId();
const currentId = currentIdStr ? parseInt(currentIdStr, 10) : null;
@@ -8780,6 +8794,10 @@ class NotificationSystem {
handleFavoritesUpdate(data) {
if (!data || !data.item_id) return;
if (window.invalidateItemCache) {
window.invalidateItemCache(data.item_id);
}
// Check if we are currently viewing this item
const currentIdStr = window.getCurrentItemId();
if (!currentIdStr || parseInt(currentIdStr, 10) !== parseInt(data.item_id, 10)) return;

View File

@@ -175,6 +175,9 @@
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;

View File

@@ -200,27 +200,27 @@ export default new class {
const tagged = +(await db`
select count(*) as total
from "items"
where id in (select item_id from tags_assign group by item_id) and active = true
where id in (select item_id from tags_assign group by item_id) and active = true and is_deleted = false
`)[0].total;
const untagged = +(await db`
select count(*) as total
from "items"
where not exists (select 1 from tags_assign where item_id = items.id) and active = true
where not exists (select 1 from tags_assign where item_id = items.id) and active = true and is_deleted = false
`)[0].total;
const sfw = +(await db`
select count(*) as total
from "items"
where id in (select item_id from tags_assign where tag_id = 1 group by item_id) and active = true
where id in (select item_id from tags_assign where tag_id = 1 group by item_id) and active = true and is_deleted = false
`)[0].total;
const nsfw = +(await db`
select count(*) as total
from "items"
where id in (select item_id from tags_assign where tag_id = 2 group by item_id) and active = true
where id in (select item_id from tags_assign where tag_id = 2 group by item_id) and active = true and is_deleted = false
`)[0].total;
const nsfl = cfg.enable_nsfl ? +(await db`
select count(*) as total
from "items"
where id in (select item_id from tags_assign where tag_id = ${cfg.nsfl_tag_id || 3} group by item_id) and active = true
where id in (select item_id from tags_assign where tag_id = ${cfg.nsfl_tag_id || 3} group by item_id) and active = true and is_deleted = false
`)[0].total : 0;
const deleted = +(await db`
select count(*) as total

View File

@@ -845,12 +845,16 @@ export default (router, tpl) => {
await fs.unlink(path.join(cfg.paths.ca, `${item.id}.webp`)).catch(() => { });
}
await db`DELETE FROM tags_assign WHERE item_id = ${item.id}`;
await db`UPDATE items SET is_deleted = true, is_purged = true, active = false WHERE id = ${item.id}`;
count++;
} catch (e) {
console.error(`[CLEANUP] Failed to delete item ${item.id}:`, e.message);
}
}
// Clean up any remaining orphan tag assignments for deleted or inactive items
await db`DELETE FROM tags_assign WHERE item_id IN (SELECT id FROM items WHERE is_deleted = true OR active = false)`;
}
// Log it in audit

View File

@@ -115,10 +115,13 @@ export default (router, tpl) => {
try {
const slug = '%' + lib.slugify(q) + '%';
const rows = await db`
SELECT t.tag, t.normalized, COUNT(ta.item_id) as uses
SELECT t.tag, t.normalized, COUNT(DISTINCT items.id) as uses
FROM tags t
JOIN tags_assign ta ON ta.tag_id = t.id
JOIN items ON items.id = ta.item_id
WHERE t.id > 2
AND items.active = true
AND items.is_deleted = false
AND lower(t.normalized) ILIKE ${slug}
GROUP BY t.tag, t.normalized
ORDER BY uses DESC

View File

@@ -33,6 +33,7 @@ export async function regenerateTagImage(tag, mode) {
${modeFilter}
WHERE (t.tag = ${tag} OR t.normalized = ${tag})
AND i.active = true
AND i.is_deleted = false
AND COALESCE(i.visibility, 0) = 0
ORDER BY RANDOM()
LIMIT 3

View File

@@ -51,6 +51,7 @@ export default (router, tpl) => {
JOIN tags_assign ta ON t.id = ta.tag_id
JOIN items ON items.id = ta.item_id
WHERE items.active = true
AND items.is_deleted = false
AND COALESCE(items.visibility, 0) = 0
AND t.id NOT IN (1, 2)
AND ${db.unsafe(modequery)}
@@ -75,6 +76,7 @@ export default (router, tpl) => {
JOIN items ON items.id = ta.item_id
WHERE t.normalized LIKE '%' || ${tag.normalized} || '%'
AND items.active = true
AND items.is_deleted = false
AND COALESCE(items.visibility, 0) = 0
AND ${db.unsafe(modequery)}
${restrictedFilter}

View File

@@ -51,6 +51,7 @@ export default (router, tpl) => {
JOIN tags_assign ta ON t.id = ta.tag_id
JOIN items ON items.id = ta.item_id
WHERE items.active = true
AND items.is_deleted = false
AND COALESCE(items.visibility, 0) = 0
AND t.id NOT IN (1, 2)
AND ta.user_id = ${userId}
@@ -76,6 +77,7 @@ export default (router, tpl) => {
JOIN items ON items.id = ta.item_id
WHERE t.normalized LIKE '%' || ${tag.normalized} || '%'
AND items.active = true
AND items.is_deleted = false
AND COALESCE(items.visibility, 0) = 0
AND ta.user_id = ${userId}
AND ${db.unsafe(modequery)}