diff --git a/src/inc/routes/chan.mjs b/src/inc/routes/chan.mjs index dbfbdd1..6df6c5a 100644 --- a/src/inc/routes/chan.mjs +++ b/src/inc/routes/chan.mjs @@ -48,13 +48,37 @@ export default (router, tpl) => { function formatComment(com) { if (!com) return ''; - // Format quote lines - let formatted = com.replace(/(?:^|)((?:>|>)((?!>|>)[^\n<]+))/g, '$1>$2'); - // Format post cross-references >>123456 - formatted = formatted.replace(/(?:>|>)>(\d+)/g, '>>$1'); + let formatted = com; + // 1. Normalize existing 4chan intra-thread quotelinks to standard format + formatted = formatted.replace(/]*href="#p(\d+)"[^>]*>.*?<\/a>/gi, '___CHANREF_$1___'); + // 2. Normalize cross-thread/cross-board quotelinks if any + formatted = formatted.replace(/]*href="(\/[^"]+)"[^>]*>(.*?)<\/a>/gi, '$2'); + // 3. Format any unlinked quotes >>123456 + formatted = formatted.replace(/(?>123456 if plain text was supplied + formatted = formatted.replace(/(?\w])>>(\d+)\b/g, '___CHANREF_$1___'); + // 5. Restore CHANREF placeholders + formatted = formatted.replace(/___CHANREF_(\d+)___/g, '>>$1'); + // 6. Format quote lines (greentext) + formatted = formatted.replace(/(^|)>(?!>)([^\n<]+)/g, '$1>$2'); return formatted; } + function extractQuotes(com) { + if (!com) return []; + const quotes = new Set(); + for (const m of com.matchAll(/href="#p(\d+)"/g)) { + quotes.add(Number(m[1])); + } + for (const m of com.matchAll(/(?\w])>>(\d+)\b/g)) { + quotes.add(Number(m[1])); + } + return Array.from(quotes); + } + // ─────────────────────────────────────────────────────────────────────────── // 0. GET /4 -> redirect to default board catalogue // ─────────────────────────────────────────────────────────────────────────── @@ -145,6 +169,21 @@ export default (router, tpl) => { }); function formatPosts(posts, board, rehostMap, userFavSet, targetPostNo = null, lang = 'en') { + // Build reply mapping (targetPostNo -> array of replier post numbers) + const postNoSet = new Set(posts.map(p => p.no)); + const replyMap = new Map(); + + for (const p of posts) { + if (!p.com) continue; + const quotes = extractQuotes(p.com); + for (const qNo of quotes) { + if (postNoSet.has(qNo) && qNo !== p.no) { + if (!replyMap.has(qNo)) replyMap.set(qNo, []); + replyMap.get(qNo).push(p.no); + } + } + } + return posts.map(p => { const ext = (p.ext || '').toLowerCase(); const isVideo = ['.webm', '.mp4'].includes(ext); @@ -154,6 +193,7 @@ export default (router, tpl) => { const localId = rehostInfo ? rehostInfo.id : null; const effectiveTime = (localId && rehostInfo?.stamp) ? Number(rehostInfo.stamp) : p.time; const isoStr = new Date(effectiveTime * 1000).toISOString(); + const postReplies = replyMap.get(p.no) || []; return { no: p.no, @@ -181,7 +221,9 @@ export default (router, tpl) => { local_id: localId, rehosted: !!localId, user_has_favorited: localId ? userFavSet.has(Number(localId)) : false, - is_onara_active: targetPostNo ? p.no === targetPostNo : false + is_onara_active: targetPostNo ? p.no === targetPostNo : false, + replies: postReplies, + replies_count: postReplies.length }; }); } diff --git a/views/chan/thread.html b/views/chan/thread.html index f7b2c7d..7cef2a0 100644 --- a/views/chan/thread.html +++ b/views/chan/thread.html @@ -56,7 +56,7 @@
@each(posts as p) -
+
@@ -74,6 +74,16 @@ @if(p.sub && p.no !== tid) {{ p.sub }} @endif + @if(p.replies && p.replies.length > 0) +
+ + + @each(p.replies as rNo) + >>{{ rNo }} + @endeach + +
+ @endif @if(p.has_media)
@@ -87,7 +97,7 @@ @if(p.has_media)
- + @if(p.thumb) Media #{{ p.no }} @else @@ -121,7 +131,7 @@ Rehosted #{{ p.local_id }} - @@ -293,7 +303,8 @@ border: 1px solid var(--border-color, rgba(255, 255, 255, 0.08)); border-radius: 10px; padding: 14px 18px; - transition: border-color 0.2s, background-color 0.2s; + transition: border-color 0.2s, background-color 0.2s, box-shadow 0.2s; + scroll-margin-top: 80px; } .chan-post-card.is-rehosted { border-color: rgba(74, 222, 128, 0.3); @@ -303,9 +314,64 @@ border-left: 4px solid #4ade80; background: rgba(30, 30, 30, 0.95); } -.chan-post-card:target { - border-color: #4ade80; - background: rgba(74, 222, 128, 0.05); +.chan-post-card.is-reply { + background: rgba(24, 24, 28, 0.8); + border: 1px solid rgba(255, 255, 255, 0.06); +} +.chan-post-card.is-reply:hover { + border-color: rgba(255, 255, 255, 0.12); +} +.chan-post-card.is-reply.has-media { + background: rgba(26, 26, 31, 0.88); + border-color: rgba(255, 255, 255, 0.08); +} +.chan-post-card:target, +.chan-post-card.active-thread-post, +.chan-post-card.onara-active { + border-color: #4ade80 !important; + background: rgba(74, 222, 128, 0.08) !important; + box-shadow: 0 0 0 2px rgba(74, 222, 128, 0.5), 0 0 20px rgba(74, 222, 128, 0.25) !important; + position: relative; + z-index: 5; +} +@keyframes chanHighlightPulse { + 0% { + border-color: #4ade80 !important; + background: rgba(74, 222, 128, 0.18) !important; + box-shadow: 0 0 0 3px #4ade80, 0 0 30px rgba(74, 222, 128, 0.6) !important; + } + 50% { + border-color: #4ade80 !important; + background: rgba(74, 222, 128, 0.10) !important; + box-shadow: 0 0 0 2px rgba(74, 222, 128, 0.7), 0 0 20px rgba(74, 222, 128, 0.4) !important; + } + 100% { + border-color: rgba(74, 222, 128, 0.4) !important; + background: rgba(74, 222, 128, 0.04) !important; + box-shadow: 0 0 0 1px rgba(74, 222, 128, 0.3) !important; + } +} +.chan-post-card.chan-flash-highlight { + animation: chanHighlightPulse 1.8s ease-out forwards !important; + position: relative; + z-index: 6; +} +.chan-thumb-link.onara-active .chan-thumb-img { + box-shadow: 0 0 0 2px #4ade80, 0 0 15px rgba(74, 222, 128, 0.6) !important; +} + +body.onara-modal-open:not(.onara-modal-closing) .chan-post-card:not(.onara-active):not(.active-thread-post) { + opacity: var(--onara-grid-dim-opacity, 0.5) !important; + transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1) !important; +} +body.onara-modal-open:not(.onara-modal-closing) .chan-post-card.onara-active, +body.onara-modal-open:not(.onara-modal-closing) .chan-post-card.active-thread-post { + opacity: 1 !important; + border-color: #4ade80 !important; + box-shadow: 0 0 0 3px #4ade80, 0 0 25px rgba(74, 222, 128, 0.5) !important; + position: relative !important; + z-index: 52 !important; + transition: box-shadow 0.28s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.25s ease !important; } .chan-post-rehosted-pill { @@ -389,85 +455,151 @@ gap: 4px; } -.chan-post-body { +/* Post Content Layout (Media + Comment) */ +.chan-post-content { display: flex; - gap: 18px; + gap: 16px; align-items: flex-start; } @media (max-width: 640px) { - .chan-post-body { + .chan-post-content { flex-direction: column; + gap: 12px; } } -/* Media Box */ -.chan-media-box { +/* Media Wrapper */ +.chan-media-wrapper { display: flex; flex-direction: column; gap: 8px; - width: 200px; + width: 170px; flex-shrink: 0; } -.chan-media-thumb-wrap { +.chan-post-card.is-reply .chan-media-wrapper { + width: 160px; +} +.chan-post-card.is-op .chan-media-wrapper { + width: 220px; +} +@media (max-width: 640px) { + .chan-media-wrapper, + .chan-post-card.is-reply .chan-media-wrapper, + .chan-post-card.is-op .chan-media-wrapper { + width: 100%; + max-width: 260px; + } +} + +/* Thumb Box & Link */ +.chan-thumb-box { position: relative; width: 100%; aspect-ratio: 16 / 10; - background: #000; + background: #0d0d0d; border-radius: 8px; overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.08); } -.chan-media-link { - display: block; - width: 100%; - height: 100%; -} -.chan-media-thumb { - width: 100%; - height: 100%; - object-fit: cover; - transition: transform 0.2s; -} -.chan-media-thumb-wrap:hover .chan-media-thumb { - transform: scale(1.05); -} -.chan-media-play-overlay { - position: absolute; - inset: 0; +.chan-thumb-link { display: flex; align-items: center; justify-content: center; - background: rgba(0, 0, 0, 0.35); - opacity: 0; - transition: opacity 0.2s; - color: #fff; + width: 100%; + height: 100%; + position: relative; + overflow: hidden; + text-decoration: none; + background: #0d0d0d; +} +.chan-thumb-img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + transition: transform 0.25s ease; +} +.chan-thumb-link:hover .chan-thumb-img { + transform: scale(1.05); +} +.chan-no-thumb { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + color: #444; font-size: 1.5rem; + background: #111; } -.chan-media-thumb-wrap:hover .chan-media-play-overlay { - opacity: 1; + +/* Video Indicator Overlay */ +.chan-video-indicator { + position: absolute; + bottom: 6px; + right: 6px; + width: 24px; + height: 24px; + border-radius: 50%; + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(4px); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.65rem; + pointer-events: none; + border: 1px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 2px 5px rgba(0,0,0,0.5); + z-index: 2; } -.chan-media-details { + +/* File Info (filename, dimensions, size) */ +.chan-file-info { display: flex; flex-direction: column; gap: 4px; - font-size: 0.75rem; + width: 100%; } -.chan-media-filename { - white-space: nowrap; +.chan-filename-row { + display: flex; + align-items: center; + overflow: hidden; +} +.chan-file-download { + display: inline-flex; + align-items: center; + gap: 5px; + color: #999; + text-decoration: none; + font-size: 0.73rem; overflow: hidden; text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; + transition: color 0.15s ease; } -.chan-media-filename a { - color: #aaa; - text-decoration: none; -} -.chan-media-filename a:hover { +.chan-file-download:hover { color: #4ade80; + text-decoration: underline; } -.chan-media-meta { - color: #777; +.chan-file-download i { + font-size: 0.68rem; + flex-shrink: 0; + opacity: 0.7; +} +.chan-file-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.chan-file-meta-row { display: flex; + align-items: center; gap: 8px; + color: #666; + font-size: 0.7rem; + font-family: monospace; } /* Rehost button */ @@ -510,6 +642,18 @@ align-items: center; width: 100%; } +.chan-rehosted-wrap { + display: inline-flex; + align-items: center; + gap: 4px; + flex: 1; + min-width: 0; +} +.chan-rehosted-wrap .chan-rehost-btn { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} .chan-card-fav-btn { display: inline-flex; align-items: center; @@ -554,16 +698,66 @@ .chan-post-comment .quote { color: #789922; } +/* Backlinks in post header */ +.chan-post-backlinks { + display: inline-flex; + align-items: center; + flex-wrap: wrap; + gap: 4px 6px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.07); + border-radius: 6px; + padding: 2px 7px; + font-size: 0.78rem; + max-width: 100%; +} +.chan-backlinks-icon { + color: #71717a; + font-size: 0.72rem; + display: inline-flex; + align-items: center; + user-select: none; +} +.chan-backlinks-icon i { + transform: scaleX(-1); +} +.chan-backlinks-items { + display: inline-flex; + align-items: center; + flex-wrap: wrap; + gap: 4px 6px; +} +.chan-backlink { + color: #f87171 !important; + text-decoration: none !important; + font-family: monospace; + font-size: 0.78rem; + cursor: pointer; + padding: 1px 5px; + border-radius: 4px; + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.2); + transition: all 0.15s ease; + line-height: 1.3; +} +.chan-backlink:hover { + color: #fff !important; + background: rgba(239, 68, 68, 0.35) !important; + border-color: rgba(239, 68, 68, 0.6) !important; + box-shadow: 0 0 8px rgba(239, 68, 68, 0.35); +} + .chan-ref, .chan-post-comment .quotelink { - color: #d00; + color: #f87171; text-decoration: underline; font-family: monospace; cursor: pointer; + transition: color 0.15s ease; } .chan-ref:hover, .chan-post-comment .quotelink:hover { - color: #ff3333; + color: #ff4d4d; } .chan-post-comment s, .chan-post-comment .spoiler { @@ -584,15 +778,41 @@ .chan-quote-preview-popup { position: fixed; z-index: 99999; - max-width: 480px; - background: #1e1e1e; + width: max-content; + max-width: 520px; + max-height: 80vh; + overflow-y: auto; + background: rgba(22, 22, 24, 0.98); border: 1px solid #4ade80; - border-radius: 8px; - padding: 12px 14px; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.7); + border-radius: 10px; + padding: 12px 16px; + box-shadow: 0 12px 35px rgba(0, 0, 0, 0.8), 0 0 20px rgba(74, 222, 128, 0.2); pointer-events: none; color: #eee; font-size: 0.85rem; + backdrop-filter: blur(12px); +} +.chan-quote-preview-popup .chan-post-header { + margin-bottom: 8px; + padding-bottom: 4px; +} +.chan-quote-preview-popup .chan-rehost-container { + display: none !important; +} +.chan-quote-preview-popup .chan-media-wrapper { + margin-bottom: 8px; +} +.chan-quote-preview-popup .chan-thumb-box { + max-width: 120px; + max-height: 120px; +} +.chan-quote-preview-popup .chan-thumb-img { + max-width: 120px; + max-height: 120px; + object-fit: cover; +} +.chan-quote-preview-popup .chan-post-backlinks { + display: none !important; } @@ -672,7 +892,7 @@ btn.outerHTML = '' + '' + ' Rehosted #' + data.item_id + '' + - ''; const postCard = document.getElementById('p' + postNo) || btn.closest('.chan-post-card'); @@ -692,7 +912,9 @@ window.showToast(data.repost ? 'Already on site — linked to existing post' : 'Rehosted successfully!'); } if (typeof window.openChanRehostModal === 'function') { - window.openChanRehostModal({ itemId: data.item_id, isPlayerView: false }); + const postCardEl = document.getElementById('p' + postNo) || postCard; + const anchor = (postCardEl ? (postCardEl.querySelector('.chan-rehost-edit-btn') || postCardEl.querySelector('.chan-rehosted-wrap')) : null) || document.getElementById('rehost-box-' + postNo); + window.openChanRehostModal({ itemId: data.item_id, anchorEl: anchor }); } } else { btn.classList.remove('loading'); @@ -713,7 +935,7 @@ e.stopImmediatePropagation(); const itemId = editBtn.dataset.itemId; if (itemId && typeof window.openChanRehostModal === 'function') { - window.openChanRehostModal({ itemId: Number(itemId), isPlayerView: false }); + window.openChanRehostModal({ itemId: Number(itemId), anchorEl: editBtn }); } } }); @@ -738,103 +960,178 @@ let localId = favBtn.dataset.localId; const mediaUrl = favBtn.dataset.url; - favBtn.classList.add('loading'); - const origHtml = favBtn.innerHTML; - favBtn.innerHTML = ''; + const wasFaved = favBtn.classList.contains('favorited'); + const isNowFav = !wasFaved; - try { - // If not yet rehosted, auto-rehost first - if (!localId && mediaUrl) { + // 1. Instantly apply client UI + favBtn.classList.toggle('favorited', isNowFav); + favBtn.innerHTML = ''; + favBtn.title = isNowFav ? 'Remove from favorites' : 'Favorite'; + if (typeof window.flashMessage === 'function') { + window.flashMessage(isNowFav ? 'ADDED TO FAVORITES' : 'REMOVED FROM FAVORITES'); + } + if (navigator.vibrate) navigator.vibrate(50); + + // 2. Perform server sync in background + (async () => { + try { + // If not yet rehosted, auto-rehost first + if (!localId && mediaUrl) { + 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: mediaUrl, + original_filename: favBtn.dataset.filename || '', + width: favBtn.dataset.width || '', + height: favBtn.dataset.height || '' + }) + }); + const rehostData = await rehostResp.json(); + if (rehostData.success && rehostData.item_id) { + localId = rehostData.item_id; + favBtn.dataset.localId = localId; + + // Update rehost box + const rehostBox = document.getElementById('rehost-box-' + postNo); + if (rehostBox) { + const rehostBtn = rehostBox.querySelector('.chan-rehost-btn'); + if (rehostBtn) { + rehostBtn.outerHTML = '' + + '' + + ' Rehosted #' + localId + '' + + ''; + } + } + // Add thumb badge if missing + const postCard = document.getElementById('p' + postNo) || favBtn.closest('.chan-post-card'); + if (postCard) { + const thumbLink = postCard.querySelector('.chan-thumb-link'); + if (thumbLink && !thumbLink.querySelector('.chan-thumb-rehost-badge')) { + const badge = document.createElement('span'); + badge.className = 'chan-thumb-rehost-badge'; + badge.title = 'Rehosted on f0ckm as Post #' + localId; + badge.innerHTML = ' #' + localId; + thumbLink.appendChild(badge); + } + const timeEl = postCard.querySelector('.chan-post-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.setAttribute('title', fullDate); + timeEl.textContent = (window.f0ckTimeAgo ? window.f0ckTimeAgo(nowIso) : (window.f0ckI18n?.timeago_just_now || 'just now')); + } + } + } else { + throw new Error(rehostData.msg || 'Rehost failed'); + } + } + + // Now sync favorite to server const csrfToken = window.f0ckSession?.csrf_token || ''; - const rehostResp = await fetch('/api/v2/scroller/rehost', { + const favResp = await fetch('/api/v2/togglefav', { method: 'POST', headers: { - 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Type': 'application/json', ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) }, - body: new URLSearchParams({ - url: mediaUrl, - original_filename: favBtn.dataset.filename || '', - width: favBtn.dataset.width || '', - height: favBtn.dataset.height || '' + body: JSON.stringify({ + postid: Number(localId), + chan_url: mediaUrl, + action: isNowFav ? 'add' : 'delete', + favorited: isNowFav }) }); - const rehostData = await rehostResp.json(); - if (rehostData.success && rehostData.item_id) { - localId = rehostData.item_id; - favBtn.dataset.localId = localId; + const favData = await favResp.json(); - // Update rehost box - const rehostBox = document.getElementById('rehost-box-' + postNo); - if (rehostBox) { - const rehostBtn = rehostBox.querySelector('.chan-rehost-btn'); - if (rehostBtn) { - rehostBtn.outerHTML = '' + - '' + - ' Rehosted #' + localId + '' + - ''; - } - } - // Add thumb badge if missing - const postCard = document.getElementById('p' + postNo) || favBtn.closest('.chan-post-card'); - if (postCard) { - const thumbLink = postCard.querySelector('.chan-thumb-link'); - if (thumbLink && !thumbLink.querySelector('.chan-thumb-rehost-badge')) { - const badge = document.createElement('span'); - badge.className = 'chan-thumb-rehost-badge'; - badge.title = 'Rehosted on f0ckm as Post #' + localId; - badge.innerHTML = ' #' + localId; - thumbLink.appendChild(badge); - } - const timeEl = postCard.querySelector('.chan-post-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.setAttribute('title', fullDate); - timeEl.textContent = (window.f0ckTimeAgo ? window.f0ckTimeAgo(nowIso) : (window.f0ckI18n?.timeago_just_now || 'just now')); - } - } + if (favData.success) { + const finalIsFav = (favData.favorited !== undefined) ? !!favData.favorited : isNowFav; + favBtn.classList.toggle('favorited', finalIsFav); + favBtn.innerHTML = ''; + favBtn.title = finalIsFav ? 'Remove from favorites' : 'Favorite'; } else { - throw new Error(rehostData.msg || 'Rehost failed'); + // Rollback on server rejection + favBtn.classList.toggle('favorited', wasFaved); + favBtn.innerHTML = ''; + favBtn.title = wasFaved ? 'Remove from favorites' : 'Favorite'; + if (typeof window.flashMessage === 'function') { + window.flashMessage(favData.msg || 'Failed to update favorite', 3000, 'error'); + } + } + } catch (err) { + console.error('[CHAN-CARD-FAV] Error:', err); + // Rollback on network/rehost error + favBtn.classList.toggle('favorited', wasFaved); + favBtn.innerHTML = ''; + favBtn.title = wasFaved ? 'Remove from favorites' : 'Favorite'; + if (typeof window.flashMessage === 'function') { + window.flashMessage('Failed to update favorite', 3000, 'error'); } } + })(); + }); - // Now toggle favorite - const csrfToken = window.f0ckSession?.csrf_token || ''; - const favResp = await fetch('/api/v2/togglefav', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) - }, - body: JSON.stringify({ postid: Number(localId), chan_url: mediaUrl }) - }); - const favData = await favResp.json(); - favBtn.classList.remove('loading'); + // Flash highlight a post card + const highlightPost = (el) => { + if (!el) return; + el.classList.remove('chan-flash-highlight'); + void el.offsetWidth; + el.classList.add('chan-flash-highlight'); + setTimeout(() => { + el.classList.remove('chan-flash-highlight'); + }, 2000); + }; - if (favData.success) { - const isNowFav = !!favData.favorited; - favBtn.classList.toggle('favorited', isNowFav); - favBtn.innerHTML = ''; - favBtn.title = isNowFav ? 'Remove from favorites' : 'Favorite'; - if (typeof window.flashMessage === 'function') { - window.flashMessage(isNowFav ? 'ADDED TO FAVORITES' : 'REMOVED FROM FAVORITES'); + // Quote click navigation (smooth jump + flash highlight) + document.addEventListener('click', (e) => { + const ref = e.target.closest('.chan-ref, .quotelink'); + if (!ref) return; + + const targetPostNo = ref.dataset.post || ref.getAttribute('href')?.replace(/^#p?/, ''); + if (!targetPostNo) return; + const targetCard = document.getElementById('p' + targetPostNo); + if (!targetCard) return; + + e.preventDefault(); + if (previewEl) previewEl.style.display = 'none'; + + // If card is hidden due to active filter ("Media" or "Rehosted"), reset to "All" + if (targetCard.style.display === 'none') { + const filterAllBtn = document.getElementById('filter-all-btn'); + if (filterAllBtn) filterAllBtn.click(); + } + + history.pushState(null, '', '#p' + targetPostNo); + try { + targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } catch (_) { + targetCard.scrollIntoView(); + } + highlightPost(targetCard); + }); + + // Handle browser back/forward buttons navigating hash + window.addEventListener('hashchange', () => { + if (window.location.hash && window.location.hash.startsWith('#p')) { + const el = document.querySelector(window.location.hash); + if (el) { + if (el.style.display === 'none') { + const filterAllBtn = document.getElementById('filter-all-btn'); + if (filterAllBtn) filterAllBtn.click(); } - } else { - favBtn.innerHTML = origHtml; - if (typeof window.flashMessage === 'function') { - window.flashMessage(favData.msg || 'Failed to update favorite', 3000, 'error'); + try { + el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } catch (_) { + el.scrollIntoView(); } - } - } catch (err) { - console.error('[CHAN-CARD-FAV] Error:', err); - favBtn.classList.remove('loading'); - favBtn.innerHTML = origHtml; - if (typeof window.flashMessage === 'function') { - window.flashMessage('Failed to update favorite', 3000, 'error'); + highlightPost(el); } } }); @@ -860,13 +1157,23 @@ previewEl.style.display = 'block'; const rect = ref.getBoundingClientRect(); - let top = rect.bottom + 8; - let left = rect.left; - if (left + 480 > window.innerWidth) left = window.innerWidth - 490; - if (top + 200 > window.innerHeight) top = rect.top - 210; + const popHeight = previewEl.offsetHeight || 150; + const popWidth = previewEl.offsetWidth || 380; - previewEl.style.top = Math.max(10, top) + 'px'; - previewEl.style.left = Math.max(10, left) + 'px'; + let top = rect.bottom + 8; + if (top + popHeight > window.innerHeight - 10) { + top = rect.top - popHeight - 8; + } + if (top < 10) top = 10; + + let left = rect.left; + if (left + popWidth > window.innerWidth - 10) { + left = window.innerWidth - popWidth - 10; + } + if (left < 10) left = 10; + + previewEl.style.top = top + 'px'; + previewEl.style.left = left + 'px'; }); document.addEventListener('mouseout', (e) => { @@ -875,6 +1182,26 @@ previewEl.style.display = 'none'; } }); + + // Jump to active post in thread if onara active or hash present + const jumpToInitialActive = () => { + let target = document.querySelector('.chan-post-card.active-thread-post, .chan-post-card.onara-active'); + if (!target && window.location.hash) { + target = document.querySelector(window.location.hash); + } + if (target) { + try { + target.scrollIntoView({ block: 'center', behavior: 'instant' }); + } catch (_) { + target.scrollIntoView(); + } + if (window.location.hash) { + highlightPost(target); + } + } + }; + jumpToInitialActive(); + setTimeout(jumpToInitialActive, 100); })();