make quick cycling great again
This commit is contained in:
@@ -3406,44 +3406,150 @@ window.cancelAnimFrame = (function () {
|
|||||||
if (!idLink) return;
|
if (!idLink) return;
|
||||||
const postid = +idLink.innerText;
|
const postid = +idLink.innerText;
|
||||||
|
|
||||||
fetch(`/api/v2/item/${postid}/rating`, {
|
// Determine current rating
|
||||||
method: 'POST',
|
let currentRating = 'sfw';
|
||||||
headers: {
|
if (toggleBtn.classList.contains('is-sfw')) currentRating = 'sfw';
|
||||||
"X-CSRF-Token": window.f0ckSession?.csrf_token
|
else if (toggleBtn.classList.contains('is-nsfw')) currentRating = 'nsfw';
|
||||||
}
|
else if (toggleBtn.classList.contains('is-nsfl')) currentRating = 'nsfl';
|
||||||
})
|
else if (toggleBtn.classList.contains('is-untagged')) currentRating = 'untagged';
|
||||||
.then(r => r.json())
|
|
||||||
.then(res => {
|
|
||||||
if (res.success) {
|
|
||||||
// Update visual state (handled by classes in css)
|
|
||||||
toggleBtn.classList.remove('is-sfw', 'is-nsfw', 'is-nsfl', 'is-untagged');
|
|
||||||
toggleBtn.classList.add(`is-${res.rating}`);
|
|
||||||
// Update label text
|
|
||||||
const labels = { sfw: 'SFW', nsfw: 'NSFW', nsfl: 'NSFL', untagged: '?' };
|
|
||||||
toggleBtn.textContent = labels[res.rating] || res.rating.toUpperCase();
|
|
||||||
|
|
||||||
window.flashMessage(`${(window.f0ckI18n && window.f0ckI18n.mode_activated && window.f0ckI18n.mode_activated.replace('{mode}', res.rating.toUpperCase())) || ('RATING UPDATED: ' + res.rating.toUpperCase())}`);
|
// Cycle SFW -> NSFW -> NSFL (if enabled) -> SFW
|
||||||
|
let nextRating = 'sfw';
|
||||||
|
if (currentRating === 'sfw') {
|
||||||
|
nextRating = 'nsfw';
|
||||||
|
} else if (currentRating === 'nsfw') {
|
||||||
|
nextRating = (window.f0ckSession && window.f0ckSession.enable_nsfl) ? 'nsfl' : 'sfw';
|
||||||
|
}
|
||||||
|
|
||||||
// Update tags in sidebar
|
const labels = { sfw: 'SFW', nsfw: 'NSFW', nsfl: 'NSFL', untagged: '?' };
|
||||||
if (window.renderTags) {
|
const nextLabel = labels[nextRating] || nextRating.toUpperCase();
|
||||||
window.renderTags(res.tags);
|
|
||||||
} else {
|
// Backup current state
|
||||||
// Fallback: manually update tags if renderTags is not global yet (or if not in admin mode)
|
const oldClasses = [...toggleBtn.classList];
|
||||||
const tagsContainer = document.getElementById('tags');
|
const oldTextContent = toggleBtn.textContent;
|
||||||
if (tagsContainer) {
|
|
||||||
// We could rebuild the whole tag list, but most items already have a rating tag
|
// Track active request ID to ignore out-of-order race conditions on rapid keypresses
|
||||||
// A simpler way is to trigger a page refresh if needed, but let's try to be subtle
|
const reqId = (toggleBtn._lastCycleReqId || 0) + 1;
|
||||||
// For now, let's just show the flash message as the toggle itself changes color
|
toggleBtn._lastCycleReqId = reqId;
|
||||||
}
|
|
||||||
|
// Increment active requests count to block incoming live SSE tag updates during cycle
|
||||||
|
toggleBtn._activeRequestsCount = (toggleBtn._activeRequestsCount || 0) + 1;
|
||||||
|
|
||||||
|
// Optimistically apply new state
|
||||||
|
toggleBtn.classList.remove('is-sfw', 'is-nsfw', 'is-nsfl', 'is-untagged');
|
||||||
|
toggleBtn.classList.add(`is-${nextRating}`);
|
||||||
|
toggleBtn.textContent = nextLabel;
|
||||||
|
|
||||||
|
// Optimistically update the sidebar tag list
|
||||||
|
let originalTags = [];
|
||||||
|
if (window.renderTags) {
|
||||||
|
const tagsContainer = document.querySelector("#tags");
|
||||||
|
const inner = tagsContainer ? (tagsContainer.querySelector(".tags-inner") || tagsContainer) : null;
|
||||||
|
if (inner) {
|
||||||
|
originalTags = [...inner.querySelectorAll(".badge")].filter(badge => {
|
||||||
|
return !badge.querySelector('#a_addtag') && !badge.querySelector('#a_toggle') && !badge.classList.contains('tag-ac-wrapper');
|
||||||
|
}).map(badge => {
|
||||||
|
const a = badge.querySelector('a[href*="/tag/"]');
|
||||||
|
const tagText = a ? a.innerText.trim() : '';
|
||||||
|
const normalized = a ? a.getAttribute('href').split('/').pop() : '';
|
||||||
|
const badgeClasses = [...badge.classList].filter(c => c !== 'badge' && c !== 'mr-2').join(' ');
|
||||||
|
return {
|
||||||
|
tag: tagText,
|
||||||
|
normalized: normalized,
|
||||||
|
badge: badgeClasses
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the optimistic rating tag object
|
||||||
|
const userStr = (window.f0ckSession && window.f0ckSession.user) || '';
|
||||||
|
const dispName = (window.f0ckSession && window.f0ckSession.display_name) || userStr;
|
||||||
|
let newRatingTag = null;
|
||||||
|
if (nextRating === 'sfw') {
|
||||||
|
newRatingTag = { id: 1, tag: 'sfw', normalized: 'sfw', badge: 'badge-success', user: userStr, display_name: dispName };
|
||||||
|
} else if (nextRating === 'nsfw') {
|
||||||
|
newRatingTag = { id: 2, tag: 'nsfw', normalized: 'nsfw', badge: 'badge-danger', user: userStr, display_name: dispName };
|
||||||
|
} else if (nextRating === 'nsfl') {
|
||||||
|
const nsfl_id = (window.f0ckSession && window.f0ckSession.nsfl_tag_id) || 3;
|
||||||
|
newRatingTag = { id: nsfl_id, tag: 'nsfl', normalized: 'nsfl', badge: 'badge-nsfl', user: userStr, display_name: dispName };
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
window.flashMessage('Error: ' + (res.msg || 'Failed to update rating'), 3000, 'error');
|
// Combine tags: filter out old rating tags and place the new one at the front
|
||||||
|
const optimisticTags = [];
|
||||||
|
if (newRatingTag) {
|
||||||
|
optimisticTags.push(newRatingTag);
|
||||||
|
}
|
||||||
|
originalTags.filter(t => t.normalized !== 'sfw' && t.normalized !== 'nsfw' && t.normalized !== 'nsfl').forEach(t => {
|
||||||
|
optimisticTags.push(t);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.renderTags(optimisticTags);
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.catch(err => {
|
|
||||||
console.error('[RATING_TOGGLE_ERROR]', err);
|
const flashMsg = (window.f0ckI18n && window.f0ckI18n.mode_activated && window.f0ckI18n.mode_activated.replace('{mode}', nextLabel)) || ('RATING UPDATED: ' + nextLabel);
|
||||||
window.flashMessage('Failed to toggle rating', 3000, 'error');
|
window.flashMessage(flashMsg);
|
||||||
|
|
||||||
|
// Enqueue the request to ensure sequential server execution in correct order
|
||||||
|
if (!toggleBtn._requestQueue) {
|
||||||
|
toggleBtn._requestQueue = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleBtn._requestQueue = toggleBtn._requestQueue.then(() => {
|
||||||
|
return fetch(`/api/v2/item/${postid}/rating`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-CSRF-Token": window.f0ckSession?.csrf_token
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ rating: nextRating })
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(res => {
|
||||||
|
toggleBtn._activeRequestsCount = Math.max(0, (toggleBtn._activeRequestsCount || 1) - 1);
|
||||||
|
|
||||||
|
// Verify we are still on the same post (prevent dynamic/PJAX page leaks)
|
||||||
|
const activeIdLink = document.querySelector("a.id-link");
|
||||||
|
const currentPostId = activeIdLink ? +activeIdLink.innerText : null;
|
||||||
|
if (currentPostId !== postid) return;
|
||||||
|
|
||||||
|
if (toggleBtn._lastCycleReqId !== reqId) return; // ignore stale responses
|
||||||
|
if (res.success) {
|
||||||
|
// Verify visual state and sync tags
|
||||||
|
toggleBtn.classList.remove('is-sfw', 'is-nsfw', 'is-nsfl', 'is-untagged');
|
||||||
|
toggleBtn.classList.add(`is-${res.rating}`);
|
||||||
|
toggleBtn.textContent = labels[res.rating] || res.rating.toUpperCase();
|
||||||
|
|
||||||
|
if (window.renderTags) {
|
||||||
|
window.renderTags(res.tags);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
revert();
|
||||||
|
window.flashMessage('Error: ' + (res.msg || 'Failed to update rating'), 3000, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
toggleBtn._activeRequestsCount = Math.max(0, (toggleBtn._activeRequestsCount || 1) - 1);
|
||||||
|
|
||||||
|
// Verify we are still on the same post (prevent dynamic/PJAX page leaks)
|
||||||
|
const activeIdLink = document.querySelector("a.id-link");
|
||||||
|
const currentPostId = activeIdLink ? +activeIdLink.innerText : null;
|
||||||
|
if (currentPostId !== postid) return;
|
||||||
|
|
||||||
|
if (toggleBtn._lastCycleReqId !== reqId) return;
|
||||||
|
console.error('[RATING_TOGGLE_ERROR]', err);
|
||||||
|
revert();
|
||||||
|
window.flashMessage('Failed to toggle rating', 3000, 'error');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function revert() {
|
||||||
|
toggleBtn.className = '';
|
||||||
|
oldClasses.forEach(cls => toggleBtn.classList.add(cls));
|
||||||
|
toggleBtn.textContent = oldTextContent;
|
||||||
|
if (window.renderTags && originalTags.length > 0) {
|
||||||
|
window.renderTags(originalTags);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -6527,6 +6633,13 @@ class NotificationSystem {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore incoming live SSE tag updates if an optimistic rating cycle is currently active on the page
|
||||||
|
const toggleBtn = document.querySelector('button#a_toggle');
|
||||||
|
if (toggleBtn && toggleBtn._activeRequestsCount > 0) {
|
||||||
|
window.f0ckDebug("[NotificationSystem] Live Tag Update ignored - Optimistic rating cycle in progress.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tagsContainer = document.querySelector('#tags');
|
const tagsContainer = document.querySelector('#tags');
|
||||||
if (!tagsContainer) {
|
if (!tagsContainer) {
|
||||||
console.warn("[NotificationSystem] #tags container not found!");
|
console.warn("[NotificationSystem] #tags container not found!");
|
||||||
|
|||||||
@@ -1064,12 +1064,22 @@ export default router => {
|
|||||||
|
|
||||||
const currentRatingId = existingRating.length > 0 ? existingRating[0].tag_id : null;
|
const currentRatingId = existingRating.length > 0 ? existingRating[0].tag_id : null;
|
||||||
let newRatingId;
|
let newRatingId;
|
||||||
if (currentRatingId === 1) {
|
const reqRating = req.body?.rating || req.post?.rating || req.url?.qs?.rating;
|
||||||
newRatingId = 2; // SFW -> NSFW
|
if (reqRating === 'sfw') {
|
||||||
} else if (currentRatingId === 2) {
|
newRatingId = 1;
|
||||||
newRatingId = cfg.enable_nsfl ? nsfl_id : 1; // NSFW -> NSFL (if enabled) or SFW
|
} else if (reqRating === 'nsfw') {
|
||||||
|
newRatingId = 2;
|
||||||
|
} else if (reqRating === 'nsfl') {
|
||||||
|
newRatingId = nsfl_id;
|
||||||
} else {
|
} else {
|
||||||
newRatingId = 1; // NSFL or none -> SFW
|
// fallback to cycling
|
||||||
|
if (currentRatingId === 1) {
|
||||||
|
newRatingId = 2; // SFW -> NSFW
|
||||||
|
} else if (currentRatingId === 2) {
|
||||||
|
newRatingId = cfg.enable_nsfl ? nsfl_id : 1; // NSFW -> NSFL (if enabled) or SFW
|
||||||
|
} else {
|
||||||
|
newRatingId = 1; // NSFL or none -> SFW
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.begin(async sql => {
|
await db.begin(async sql => {
|
||||||
|
|||||||
@@ -105,8 +105,19 @@ export default router => {
|
|||||||
const cycle = [1, 2, nsflId];
|
const cycle = [1, 2, nsflId];
|
||||||
const currentTags = await lib.getTags(postid);
|
const currentTags = await lib.getTags(postid);
|
||||||
const ratingTagId = currentTags.find(t => [1, 2, nsflId].includes(t.id))?.id ?? 0;
|
const ratingTagId = currentTags.find(t => [1, 2, nsflId].includes(t.id))?.id ?? 0;
|
||||||
const cycleIdx = cycle.indexOf(ratingTagId); // -1 if untagged → (−1+1)%3 = 0 → SFW
|
|
||||||
const nextTagId = cycle[(cycleIdx + 1) % cycle.length];
|
let nextTagId;
|
||||||
|
const reqRating = req.body?.rating || req.post?.rating || req.url?.qs?.rating;
|
||||||
|
if (reqRating === 'sfw') {
|
||||||
|
nextTagId = 1;
|
||||||
|
} else if (reqRating === 'nsfw') {
|
||||||
|
nextTagId = 2;
|
||||||
|
} else if (reqRating === 'nsfl') {
|
||||||
|
nextTagId = nsflId;
|
||||||
|
} else {
|
||||||
|
const cycleIdx = cycle.indexOf(ratingTagId); // -1 if untagged → (−1+1)%3 = 0 → SFW
|
||||||
|
nextTagId = cycle[(cycleIdx + 1) % cycle.length];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Remove any existing rating tag
|
// Remove any existing rating tag
|
||||||
|
|||||||
@@ -387,6 +387,7 @@
|
|||||||
disable_swiping: @if(session && session.disable_swiping) true @else false @endif,
|
disable_swiping: @if(session && session.disable_swiping) true @else false @endif,
|
||||||
show_background: @if(session)@if(session.show_background !== false) true @else false @endif@else @if(show_background_cfg) true @else false @endif@endif,
|
show_background: @if(session)@if(session.show_background !== false) true @else false @endif@else @if(show_background_cfg) true @else false @endif@endif,
|
||||||
nsfl_tag_id: {{ nsfl_tag_id }},
|
nsfl_tag_id: {{ nsfl_tag_id }},
|
||||||
|
enable_nsfl: @if(enable_nsfl) true @else false @endif,
|
||||||
user: @if(session) "{{ session.user }}" @else null @endif,
|
user: @if(session) "{{ session.user }}" @else null @endif,
|
||||||
display_name: @if(session && session.display_name) "{!! session.display_name !!}" @else null @endif,
|
display_name: @if(session && session.display_name) "{!! session.display_name !!}" @else null @endif,
|
||||||
is_admin: @if(session && session.admin) true @else false @endif,
|
is_admin: @if(session && session.admin) true @else false @endif,
|
||||||
|
|||||||
Reference in New Issue
Block a user