diff --git a/extension/content.js b/extension/content.js index bdc5fb6..1da57a3 100644 --- a/extension/content.js +++ b/extension/content.js @@ -189,6 +189,59 @@ function clearTrackerElementForPost(postElement, trackerElement = null) { trackerElementsByPost.delete(postElement); } +function getTrackerScopeRoot(element) { + return element ? element.closest(DIALOG_ROOT_SELECTOR) : null; +} + +function getTrackersForUrlInScope(encodedUrl, scopeRoot) { + if (!encodedUrl) { + return []; + } + + const selector = `.fb-tracker-ui[data-post-url="${encodedUrl}"]`; + const trackers = Array.from(document.querySelectorAll(selector)).filter((tracker) => tracker && tracker.isConnected); + + return trackers.filter((tracker) => { + const trackerScopeRoot = getTrackerScopeRoot(tracker); + if (scopeRoot) { + return trackerScopeRoot === scopeRoot; + } + return !trackerScopeRoot; + }); +} + +function dedupeTrackersForUrlInScope(encodedUrl, scopeRoot, preferredTracker = null) { + const trackers = getTrackersForUrlInScope(encodedUrl, scopeRoot); + if (!trackers.length) { + return null; + } + + const keep = preferredTracker && trackers.includes(preferredTracker) + ? preferredTracker + : trackers[trackers.length - 1]; + + trackers.forEach((tracker) => { + if (tracker !== keep) { + tracker.remove(); + } + }); + + return keep; +} + +function getTrackerInsertionAnchorInPost(postElement, buttonBar) { + if (!postElement || !buttonBar || !postElement.contains(buttonBar)) { + return null; + } + + let anchor = buttonBar; + while (anchor.parentElement && anchor.parentElement !== postElement) { + anchor = anchor.parentElement; + } + + return anchor && anchor.parentElement === postElement ? anchor : null; +} + const AI_CREDENTIAL_CACHE_TTL = 60 * 1000; // 1 minute cache const aiCredentialCache = { data: null, @@ -3074,6 +3127,18 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options = console.log('[FB Tracker] Post #' + postNum + ' - Creating tracker UI for:', postUrlData.url, postElement); const encodedUrl = encodeURIComponent(postUrlData.url); + const scopeRoot = getTrackerScopeRoot(postElement); + + const existingScopedTrackers = getTrackersForUrlInScope(encodedUrl, scopeRoot); + if (existingScopedTrackers.length > 0) { + const scopedTracker = dedupeTrackersForUrlInScope(encodedUrl, scopeRoot, existingScopedTrackers[0]); + if (scopedTracker) { + console.log('[FB Tracker] Post #' + postNum + ' - Reusing existing tracker UI in same scope'); + setTrackerElementForPost(postElement, scopedTracker); + postElement.setAttribute(PROCESSED_ATTR, '1'); + return; + } + } const existingEntry = processedPostUrls.get(encodedUrl); if (existingEntry && existingEntry.element && existingEntry.element !== postElement) { @@ -3083,10 +3148,7 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options = } else { processedPostUrls.delete(encodedUrl); } - const otherUI = document.querySelector(`.fb-tracker-ui[data-post-url="${encodedUrl}"]`); - if (otherUI) { - otherUI.remove(); - } + getTrackersForUrlInScope(encodedUrl, scopeRoot).forEach((tracker) => tracker.remove()); } const { likeButton: sourceLikeButton = null, isSearchResult = false, isDialogContext = false } = options; @@ -3114,6 +3176,8 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options = row-gap: 6px; width: 100%; box-sizing: border-box; + position: relative; + z-index: 2; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 13px; `; @@ -3726,20 +3790,29 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options = inserted = tryInsertBeforeReelsCommentComposer(); } - // Strategy 1: After button bar's parent (more stable) - if (!inserted && buttonBar && buttonBar.parentElement && buttonBar.parentElement.parentElement) { + // Strategy 1: Insert right after the action block inside the current post container + if (!inserted && buttonBar) { + const actionAnchor = getTrackerInsertionAnchorInPost(postElement, buttonBar); + if (actionAnchor && actionAnchor.parentElement) { + actionAnchor.parentElement.insertBefore(container, actionAnchor.nextSibling); + console.log('[FB Tracker] Post #' + postNum + ' - UI inserted after in-post action block. ID: #' + container.id); + inserted = true; + } + } + // Strategy 2: Insert directly after the button bar if still inside the post container + if (!inserted && buttonBar && buttonBar.parentElement && postElement.contains(buttonBar.parentElement)) { + buttonBar.parentElement.insertBefore(container, buttonBar.nextSibling); + console.log('[FB Tracker] Post #' + postNum + ' - UI inserted after button bar (in-post fallback). ID: #' + container.id); + inserted = true; + } + // Strategy 3: Legacy fallback for old variants where the interaction bar is outside the post container + if (!inserted && buttonBar && buttonBar.parentElement && buttonBar.parentElement.parentElement && !postElement.contains(buttonBar.parentElement.parentElement)) { const grandParent = buttonBar.parentElement.parentElement; grandParent.insertBefore(container, buttonBar.parentElement.nextSibling); - console.log('[FB Tracker] Post #' + postNum + ' - UI inserted after button bar parent. ID: #' + container.id); + console.log('[FB Tracker] Post #' + postNum + ' - UI inserted after external button bar parent (legacy). ID: #' + container.id); inserted = true; } - // Strategy 2: After button bar directly - if (!inserted && buttonBar && buttonBar.parentElement) { - buttonBar.parentElement.insertBefore(container, buttonBar.nextSibling); - console.log('[FB Tracker] Post #' + postNum + ' - UI inserted after button bar. ID: #' + container.id); - inserted = true; - } - // Strategy 3: Append to post element + // Strategy 4: Append to post element if (!inserted) { postElement.appendChild(container); console.log('[FB Tracker] Post #' + postNum + ' - UI inserted into article (fallback). ID: #' + container.id); @@ -3747,6 +3820,11 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options = } if (inserted) { + const dedupedTracker = dedupeTrackersForUrlInScope(encodedUrl, scopeRoot, container); + if (dedupedTracker && dedupedTracker !== container) { + clearTrackerElementForPost(postElement, container); + } + processedPostUrls.set(encodedUrl, { element: postElement, createdAt: Date.now(), @@ -3755,7 +3833,7 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options = : null, hidden: false }); - setTrackerElementForPost(postElement, container); + setTrackerElementForPost(postElement, dedupedTracker || container); } // Monitor if the UI gets removed and re-insert it @@ -3765,12 +3843,18 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options = console.log('[FB Tracker] Post #' + postNum + ' - UI was removed, re-inserting...'); observer.disconnect(); // Try to re-insert - if (buttonBar && buttonBar.parentElement && buttonBar.parentElement.parentElement) { + const actionAnchor = getTrackerInsertionAnchorInPost(postElement, buttonBar); + if (actionAnchor && actionAnchor.parentElement) { + actionAnchor.parentElement.insertBefore(container, actionAnchor.nextSibling); + } else if (buttonBar && buttonBar.parentElement && postElement.contains(buttonBar.parentElement)) { + buttonBar.parentElement.insertBefore(container, buttonBar.nextSibling); + } else if (buttonBar && buttonBar.parentElement && buttonBar.parentElement.parentElement && !postElement.contains(buttonBar.parentElement.parentElement)) { buttonBar.parentElement.parentElement.insertBefore(container, buttonBar.parentElement.nextSibling); - } else if (postElement.parentElement) { - postElement.parentElement.appendChild(container); + } else if (postElement) { + postElement.appendChild(container); } if (container.isConnected) { + dedupeTrackersForUrlInScope(encodedUrl, scopeRoot, container); setTrackerElementForPost(postElement, container); } }