diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css
index cff124b..06237ce 100644
--- a/public/s/css/f0ckm.css
+++ b/public/s/css/f0ckm.css
@@ -10277,6 +10277,16 @@ table img {
border-color: rgba(255, 107, 157, 0.4) !important;
}
+@keyframes fav-pop {
+ 0% { transform: scale(1); }
+ 45% { transform: scale(1.32); }
+ 100% { transform: scale(1); }
+}
+
+.fav-pop {
+ animation: fav-pop 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important;
+}
+
/* pin icon */
.iconset#a_pin {
color: inherit;
@@ -17155,6 +17165,26 @@ body.layout-legacy .gapRight>.iconset {
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, opacity 0.15s ease;
}
+.gapRight i.iconset.fa-regular,
+.gapRight svg.iconset.fa-regular,
+.gapRight i.iconset.far,
+.gapRight i.fa-regular,
+.gapRight i.far,
+body.layout-legacy .gapRight>i.iconset.fa-regular,
+body.layout-modern .gapRight>i.iconset.fa-regular {
+ font-weight: 400 !important;
+}
+
+.gapRight i.iconset.fa-regular::before,
+.gapRight svg.iconset.fa-regular::before,
+.gapRight i.iconset.far::before,
+.gapRight i.fa-regular::before,
+.gapRight i.far::before,
+body.layout-legacy .gapRight>i.iconset.fa-regular::before,
+body.layout-modern .gapRight>i.iconset.fa-regular::before {
+ font-weight: 400 !important;
+}
+
.gapRight>svg.iconset:hover,
.gapRight>i.iconset:hover,
.gapRight>button:hover,
@@ -17618,6 +17648,33 @@ textarea#profile_description {
transform: scale(1.1);
}
+.user-infobox-block #a_favo.fa-regular,
+.user-infobox-block .gapRight #a_favo.fa-regular,
+.user-infobox-block .user-infobox-actions #a_favo.fa-regular,
+body.layout-legacy .user-infobox-block .gapRight > #a_favo.fa-regular,
+body.layout-modern .user-infobox-block .gapRight > #a_favo.fa-regular {
+ font-weight: 400 !important;
+ background: transparent !important;
+ background-color: transparent !important;
+}
+
+.user-infobox-block #a_favo.fa-regular:hover,
+.user-infobox-block .gapRight #a_favo.fa-regular:hover,
+.user-infobox-block .user-infobox-actions #a_favo.fa-regular:hover,
+body.layout-legacy .user-infobox-block .gapRight > #a_favo.fa-regular:hover,
+body.layout-modern .user-infobox-block .gapRight > #a_favo.fa-regular:hover {
+ background: transparent !important;
+ background-color: transparent !important;
+}
+
+.user-infobox-block #a_favo.fa-regular::before,
+.user-infobox-block .gapRight #a_favo.fa-regular::before,
+.user-infobox-block .user-infobox-actions #a_favo.fa-regular::before,
+body.layout-legacy .user-infobox-block .gapRight > #a_favo.fa-regular::before,
+body.layout-modern .user-infobox-block .gapRight > #a_favo.fa-regular::before {
+ font-weight: 400 !important;
+}
+
.user-infobox-username {
font-weight: 700;
color: var(--author-accent, inherit) !important;
@@ -22487,20 +22544,146 @@ div#flash,
}
}
-/* Chan Rehost Details Modal */
+/* Chan Rehost Details Modal / Popup */
.chan-rehost-modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
- background: rgba(0, 0, 0, 0.82);
- backdrop-filter: blur(6px);
+ background: transparent;
+ backdrop-filter: none;
z-index: 100200;
+ display: block;
+}
+
+.chan-rehost-modal .chan-rehost-modal-content,
+.chan-rehost-modal.chan-rehost-popup-mode .chan-rehost-modal-content {
+ position: fixed !important;
+ width: 270px !important;
+ max-width: calc(100vw - 20px) !important;
+ background: #18181b !important;
+ border: 1px solid rgba(255, 255, 255, 0.18) !important;
+ border-radius: 10px !important;
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.85), 0 0 0 1px rgba(255, 255, 255, 0.08) !important;
+ padding: 10px 12px !important;
+ text-align: left !important;
+ box-sizing: border-box !important;
+ animation: chanRehostPopupIn 0.12s cubic-bezier(0.16, 1, 0.3, 1);
+ z-index: 100201 !important;
+}
+
+@keyframes chanRehostPopupIn {
+ from {
+ opacity: 0;
+ transform: scale(0.96) translateY(3px);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
+.chan-rehost-popup-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 8px;
+ padding-bottom: 6px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.chan-rehost-popup-title {
+ font-size: 0.78rem;
+ font-weight: 700;
+ color: #d4d4d8;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.chan-rehost-popup-title i {
+ color: #4ade80;
+ font-size: 0.82rem;
+}
+
+.chan-rehost-popup-close {
+ background: none;
+ border: none;
+ color: #71717a;
+ font-size: 1.15rem;
+ cursor: pointer;
+ padding: 0 4px;
+ line-height: 1;
+ transition: color 0.15s;
+}
+
+.chan-rehost-popup-close:hover {
+ color: #fff;
+}
+
+.chan-rehost-ratings.chan-rehost-popup-ratings {
+ display: flex;
+ gap: 4px;
+ margin-bottom: 8px;
+}
+
+.chan-rehost-ratings.chan-rehost-popup-ratings .chan-rehost-pill {
+ flex: 1;
+ padding: 6px 2px;
+ font-size: 0.72rem;
+ font-weight: 600;
+ border-radius: 5px;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ background: rgba(255, 255, 255, 0.05);
+ color: #a1a1aa;
+ cursor: pointer;
+ transition: all 0.12s ease;
display: flex;
align-items: center;
justify-content: center;
+ gap: 3px;
+ white-space: nowrap;
}
+
+.chan-rehost-ratings.chan-rehost-popup-ratings .chan-rehost-pill:hover {
+ background: rgba(255, 255, 255, 0.12);
+ color: #fff;
+}
+
+.chan-rehost-popup-actions {
+ display: flex;
+ gap: 6px;
+}
+
+.chan-rehost-popup-save-btn {
+ width: 100%;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ background: #16a34a !important;
+ color: #fff !important;
+ border: none !important;
+ border-radius: 6px;
+ padding: 6px 12px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.15s, opacity 0.15s;
+}
+
+.chan-rehost-popup-save-btn:hover {
+ background: #15803d !important;
+}
+
+.chan-rehost-popup-save-btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
.chan-rehost-modal-content {
background: #18181b;
border: 1px solid rgba(255, 255, 255, 0.14);
diff --git a/public/s/js/admin.js b/public/s/js/admin.js
index 414b670..74a7add 100644
--- a/public/s/js/admin.js
+++ b/public/s/js/admin.js
@@ -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 = `
-
- #${localId}
-
-
- `;
+ 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 = `
+
+ #${localId}
+
+
+ `;
+ }
+ } 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) => {
diff --git a/public/s/js/anon_ssh.js b/public/s/js/anon_ssh.js
index 83539e5..e613618 100644
--- a/public/s/js/anon_ssh.js
+++ b/public/s/js/anon_ssh.js
@@ -578,7 +578,12 @@
if (typeof window.showToastNotification === 'function') window.showToastNotification('New passkey registered!');
this._refreshModalContent();
} catch (err) {
- if (err.name !== 'NotAllowedError') alert('Failed to add passkey: ' + err.message);
+ console.warn('[ANON_PASSKEY] Modal add passkey error:', err);
+ if (err && err.name === 'NotAllowedError') {
+ alert('Passkey creation was cancelled or blocked. If Bitwarden or another password manager is locked, please unlock it.');
+ } else {
+ alert('Failed to add passkey: ' + ((err && err.message) || 'Unknown error'));
+ }
} finally {
addBtn.disabled = false;
}
diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js
index 48675e9..81def84 100644
--- a/public/s/js/f0ckm.js
+++ b/public/s/js/f0ckm.js
@@ -1924,6 +1924,39 @@ window.cancelAnimFrame = (function () {
// Clear onara-active from any and all elements to guarantee only 1 item is selected
document.querySelectorAll('.onara-active').forEach(el => el.classList.remove('onara-active'));
+ // Check for 4chan thread post card in the background thread
+ let postNo = null;
+ if (url) {
+ const m = new URL(url, window.location.origin).pathname.match(/^\/4\/[a-z0-9_]+\/\d+\/(\d+)/i);
+ if (m) postNo = m[1];
+ }
+ if (!postNo && itemid && /^\d+$/.test(String(itemid))) {
+ postNo = String(itemid);
+ }
+
+ let targetChanCard = null;
+ if (postNo) {
+ targetChanCard = document.getElementById('p' + postNo) || document.querySelector(`.chan-post-card[data-no="${postNo}"]`);
+ }
+
+ if (targetChanCard) {
+ document.querySelectorAll('.chan-post-card.active-thread-post').forEach(el => el.classList.remove('active-thread-post'));
+ targetChanCard.classList.add('active-thread-post', 'onara-active');
+ const chanThumb = targetChanCard.querySelector('.chan-thumb-link');
+ if (chanThumb) chanThumb.classList.add('onara-active');
+
+ if (document.activeElement && document.activeElement !== targetChanCard && typeof document.activeElement.blur === 'function') {
+ document.activeElement.blur();
+ }
+
+ try {
+ targetChanCard.scrollIntoView({ block: 'center', behavior: forceScroll ? 'instant' : 'smooth' });
+ } catch (_) {
+ targetChanCard.scrollIntoView();
+ }
+ return targetChanCard;
+ }
+
let targetThumb = null;
if (url) {
try {
@@ -2177,6 +2210,47 @@ window.cancelAnimFrame = (function () {
const currentSeq = ++_onaraSyncSeq;
const urlObj = new URL(url || window.location.href, window.location.origin);
+ if (urlObj.pathname.startsWith('/4/')) {
+ const chanStream = document.querySelector('.chan-posts-stream');
+ const chanM = urlObj.pathname.match(/^\/4\/([a-z0-9_]+)\/(\d+)/i);
+ let sameThread = false;
+ if (chanStream && chanM) {
+ const currentUrlMatch = window.location.pathname.match(/^\/4\/([a-z0-9_]+)\/(\d+)/i);
+ if (currentUrlMatch && currentUrlMatch[1].toLowerCase() === chanM[1].toLowerCase() && currentUrlMatch[2] === chanM[2]) {
+ sameThread = true;
+ } else if (document.querySelector('.chan-back-btn, .sidebar-chan-overview-btn, a[href*="/4/' + chanM[1] + '/' + chanM[2] + '"]')) {
+ sameThread = true;
+ }
+ }
+ if (sameThread) {
+ updateOnaraActiveItem(itemid, url, false, slug, extraMeta);
+ return;
+ }
+ if (chanM) {
+ const threadUrl = `/4/${chanM[1]}/${chanM[2]}`;
+ try {
+ const pageRes = await fetch(threadUrl, {
+ headers: { 'X-Requested-With': 'XMLHttpRequest' }
+ });
+ if (pageRes.ok) {
+ const html = await pageRes.text();
+ const doc = new DOMParser().parseFromString(html, 'text/html');
+ const main = document.getElementById('main');
+ const newMain = doc.querySelector('#main');
+ if (main && newMain) {
+ main.className = newMain.className;
+ main.innerHTML = newMain.innerHTML;
+ window._onaraReturnUrl = threadUrl;
+ if (doc.title) window._onaraReturnTitle = doc.title;
+ updateOnaraActiveItem(itemid, url, true, slug, extraMeta);
+ }
+ }
+ } catch (e) {
+ console.warn('[ONARA] Failed to sync chan thread:', e);
+ }
+ }
+ return;
+ }
const feedBasePath = urlObj.pathname.replace(/\/+$/, '').replace(/\/(?:\d+|[a-zA-Z0-9_-]{11})$/, '') || '/';
let targetPage = (typeof knownPage === 'number' && knownPage >= 1) ? knownPage : null;
@@ -2364,6 +2438,10 @@ window.cancelAnimFrame = (function () {
} catch {}
}
+ if (path.startsWith('/4/')) {
+ return path;
+ }
+
const postsEl = document.querySelector('.posts');
const activePagSpan = document.querySelector('.pagination span.btn.disabled, .pagination span.disabled');
const pagPage = activePagSpan ? parseInt(activePagSpan.textContent.trim(), 10) : 0;
@@ -2452,7 +2530,7 @@ window.cancelAnimFrame = (function () {
window._onaraReturnTitle = null;
// Maintain focus on active item so user TAB navigation seamlessly resumes from current progress
- const activeThumb = document.querySelector('.posts > a.thumb.onara-active');
+ const activeThumb = document.querySelector('.posts > a.thumb.onara-active, .chan-post-card.active-thread-post .chan-thumb-link, .chan-post-card.onara-active .chan-thumb-link');
if (activeThumb) {
try {
activeThumb.focus({ preventScroll: true });
@@ -4437,7 +4515,7 @@ window.cancelAnimFrame = (function () {
const doc = new DOMParser().parseFromString(html, 'text/html');
const gridContent = doc.querySelector('#main') || doc.body;
if (gridContent) {
- main.className = '';
+ main.className = gridContent.className || '';
main.innerHTML = gridContent.innerHTML;
if (typeof window.initLazyLoading === 'function') {
window.initLazyLoading();
@@ -4452,7 +4530,7 @@ window.cancelAnimFrame = (function () {
}
if (activeKey) {
- updateOnaraActiveItem(activeKey, window.location.href);
+ updateOnaraActiveItem(activeKey, window.location.href, true);
if (typeof window.trackVisit === 'function') {
window.trackVisit(activeKey);
}
@@ -4473,7 +4551,7 @@ window.cancelAnimFrame = (function () {
const keySegments = pathSegments.filter(s => /^\d+$/.test(s) || /^[a-zA-Z0-9_-]{11}$/.test(s));
const activeKey = keySegments.length ? keySegments[keySegments.length - 1] : null;
if (activeKey) {
- updateOnaraActiveItem(activeKey, window.location.href);
+ updateOnaraActiveItem(activeKey, window.location.href, true);
if (typeof window.trackVisit === 'function') {
window.trackVisit(activeKey);
}
@@ -11261,7 +11339,10 @@ window.cancelAnimFrame = (function () {
if (isItem) {
isNavigating = false;
- return loadItemAjax(url, replace, options);
+ const shouldInherit = (options.skipInherit || options.inheritContext === false) ? false : (options.inheritContext !== undefined ? !!options.inheritContext : false);
+ const itemOptions = { ...options };
+ if (replace) itemOptions.replace = true;
+ return loadItemAjax(url, shouldInherit, itemOptions);
}
@@ -12369,7 +12450,12 @@ window.cancelAnimFrame = (function () {
// Prev/next arrows in item view always use explicit /h/foo/ or /tag/foo/ hrefs
// (via link.main), so they are unaffected by this guard.
// Note: Notification links call loadItemAjax(href, false) to skip inheritance directly.
- if (inheritContext && !isChanUrl) {
+ const targetUrlObj = new URL(url, window.location.origin);
+ const targetPath = targetUrlObj.pathname;
+ const hasContextPrefix = /^\/(?:tag|h|user|favs|4)\//.test(targetPath);
+ const isBareItem = !hasContextPrefix;
+
+ if (inheritContext && !isChanUrl && !isBareItem && !options.skipInherit) {
if (!tag) {
const wTagMatch = window.location.href.match(/\/tag\/([^/?]+)/);
if (wTagMatch) tag = decodeURIComponent(wTagMatch[1]);
@@ -12592,6 +12678,8 @@ window.cancelAnimFrame = (function () {
promoteOnaraModals();
const onaraModal = document.getElementById('onara-modal');
if (onaraModal) onaraModal.scrollTo(0, 0);
+ updateOnaraActiveItem(itemid, url, false, _cachedItem.slug, null);
+ syncOnaraBackgroundGrid(itemid, url, _cachedItem.page, _cachedItem.slug, null);
} else if (options.skipPush && history.state?.scroll !== undefined) {
requestAnimationFrame(() => window.scrollTo(0, history.state.scroll));
} else if (!options.keepMedia) {
@@ -12601,7 +12689,10 @@ window.cancelAnimFrame = (function () {
const _hash = new URL(url, window.location.origin).hash;
const itemKey = (window.f0ckSession?.enable_item_slugs !== false && _cachedItem.slug) ? _cachedItem.slug : (_cachedItem.slug || itemid);
let _pushUrl = `/${itemKey}`;
- if (userHall && userHallOwner) _pushUrl = `/user/${encodeURIComponent(userHallOwner)}/hall/${encodeURIComponent(userHall)}/${itemKey}`;
+ if (isChanUrl) {
+ const u = new URL(url, window.location.origin);
+ _pushUrl = u.pathname + u.search + u.hash;
+ } else if (userHall && userHallOwner) _pushUrl = `/user/${encodeURIComponent(userHallOwner)}/hall/${encodeURIComponent(userHall)}/${itemKey}`;
else if (user) { _pushUrl = `/user/${encodeURIComponent(user)}/${itemKey}`; if (isFavs) _pushUrl = `/user/${encodeURIComponent(user)}/favs/${itemKey}`; }
else if (isFavs) _pushUrl = `/favs/${itemKey}`;
else if (tag) _pushUrl = `/tag/${encodeURIComponent(tag).replace(/%2C/g,',').replace(/%20/g,' ')}/${itemKey}`;
@@ -12952,7 +13043,11 @@ window.cancelAnimFrame = (function () {
if (!options.keepMedia && !options.skipPush) {
// We overwrite proper URL even if the link clicked was "naked"
- history.pushState({ onara: useOnara }, '', pushUrl);
+ if (options.replace || options.replaceState) {
+ history.replaceState({ onara: useOnara }, '', pushUrl);
+ } else {
+ history.pushState({ onara: useOnara }, '', pushUrl);
+ }
}
if (!options.keepMedia) {
@@ -13507,121 +13602,195 @@ window.cancelAnimFrame = (function () {
}
}
- window.openChanRehostModal = function({ itemId, isPlayerView = false, onSave = null }) {
+ window.openChanRehostModal = function({ itemId, onSave = null, anchorEl = null }) {
if (!itemId) return;
let modal = document.getElementById('chan-rehost-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'chan-rehost-modal';
- modal.className = 'modal-overlay chan-rehost-modal';
- modal.style.display = 'none';
document.body.appendChild(modal);
}
+ if (!anchorEl) {
+ anchorEl = document.querySelector(`.chan-rehost-edit-btn[data-item-id="${itemId}"]`) ||
+ document.querySelector(`.chan-rehost-btn[href="/${itemId}"]`) ||
+ document.querySelector(`.chan-rehost-action-btn[href="/${itemId}"]`) ||
+ document.querySelector(`#rehost-box-${itemId} .chan-rehosted-wrap`) ||
+ document.querySelector(`#rehost-box-${itemId}`) ||
+ document.getElementById('chan-item-rehost-btn');
+ }
+
+ modal.className = 'modal-overlay chan-rehost-modal chan-rehost-popup-mode';
+ modal.style.display = 'block';
+ document.body.classList.remove('modal-open');
+
modal.innerHTML = `
-
-