From c0efa3f1fc1f8b6cb205243a3f76e0873cb5f5b8 Mon Sep 17 00:00:00 2001 From: Meik Date: Thu, 26 Feb 2026 00:49:38 +0100 Subject: [PATCH] Make tracker anchoring self-healing against comment lazy-load --- extension/content.js | 148 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 131 insertions(+), 17 deletions(-) diff --git a/extension/content.js b/extension/content.js index 297bd12..c9ad1a2 100644 --- a/extension/content.js +++ b/extension/content.js @@ -309,6 +309,13 @@ function getTrackerInsertionAnchorInPost(postElement, buttonBar) { return anchor && anchor.parentElement === postElement ? anchor : null; } +function isNodeAfter(node, referenceNode) { + if (!node || !referenceNode || node === referenceNode) { + return false; + } + return Boolean(node.compareDocumentPosition(referenceNode) & Node.DOCUMENT_POSITION_PRECEDING); +} + function hasCommentComposerSignal(node) { if (!node || !node.querySelector) { return false; @@ -321,6 +328,34 @@ function hasCommentComposerSignal(node) { ); } +function findCommentComposerAnchorInPost(postElement) { + if (!postElement) { + return null; + } + + const composerCandidates = Array.from( + postElement.querySelectorAll( + 'div[role="textbox"][contenteditable="true"], [role="textbox"][aria-label*="komment" i], [role="textbox"][aria-label*="comment" i], [role="textbox"][aria-placeholder*="komment" i], [role="textbox"][aria-placeholder*="comment" i]' + ) + ); + + for (const composer of composerCandidates) { + if (!composer || composer.closest('.fb-tracker-ui')) { + continue; + } + + const composerRoot = composer.closest('form[role="presentation"]') + || composer.closest('form') + || composer; + const topLevelAnchor = getTopLevelAnchorWithinPost(postElement, composerRoot) || composerRoot; + if (topLevelAnchor && topLevelAnchor.parentElement && postElement.contains(topLevelAnchor)) { + return topLevelAnchor; + } + } + + return null; +} + function countActionRoleSignals(node) { if (!node || !node.querySelector) { return 0; @@ -364,13 +399,28 @@ function findActionBarByDataRoles(postElement) { return null; } + const commentBoundary = findCommentComposerAnchorInPost(postElement) + || findCommentSortAnchorInPost(postElement); const markers = Array.from(postElement.querySelectorAll( '[data-ad-rendering-role="like_button"], [data-ad-rendering-role*="gefällt" i], [data-ad-rendering-role="comment_button"], [data-ad-rendering-role="share_button"]' )); for (const marker of markers) { + if ( + marker.closest('[aria-roledescription*="comment" i], [aria-roledescription*="kommentar" i], [data-testid*="comment" i], [data-scope="comment"]') + ) { + continue; + } + if (commentBoundary && isNodeAfter(marker, commentBoundary)) { + continue; + } + let current = marker; for (let depth = 0; depth < 8 && current && current !== postElement; depth++) { + if (commentBoundary && isNodeAfter(current, commentBoundary)) { + current = current.parentElement; + continue; + } if (isReliableActionBarCandidate(postElement, current)) { return current; } @@ -528,6 +578,21 @@ function findCommentSortAnchorInPost(postElement) { return null; } +function findCommentBoundaryAnchorInPost(postElement, actionAnchor = null) { + if (!postElement) { + return null; + } + + const adjacentSortAnchor = actionAnchor + ? findAdjacentCommentSortAnchor(postElement, actionAnchor) + : null; + if (adjacentSortAnchor) { + return adjacentSortAnchor; + } + + return findCommentSortAnchorInPost(postElement) || findCommentComposerAnchorInPost(postElement); +} + function createTrackerInsertionMarker() { const marker = document.createElement('div'); marker.className = 'fb-tracker-insertion-anchor'; @@ -552,10 +617,16 @@ function positionTrackerInsertionMarker(postElement, marker, buttonBar, postNum const resolvedButtonBar = resolveActionButtonBar(postElement, buttonBar); const actionAnchor = getDirectActionAnchorInPost(postElement, resolvedButtonBar); - const preferredAnchor = actionAnchor; + const commentBoundary = findCommentBoundaryAnchorInPost(postElement, actionAnchor); - if (preferredAnchor && preferredAnchor.parentElement) { - preferredAnchor.parentElement.insertBefore(marker, preferredAnchor.nextSibling); + if (actionAnchor && commentBoundary && commentBoundary.parentElement && isNodeAfter(commentBoundary, actionAnchor)) { + commentBoundary.parentElement.insertBefore(marker, commentBoundary); + setTrackerInsertionMarkerLocked(marker, true); + return true; + } + + if (actionAnchor && actionAnchor.parentElement) { + actionAnchor.parentElement.insertBefore(marker, actionAnchor.nextSibling); setTrackerInsertionMarkerLocked(marker, true); return true; } @@ -572,11 +643,10 @@ function positionTrackerInsertionMarker(postElement, marker, buttonBar, postNum return true; } - const fallbackSortAnchor = findCommentSortAnchorInPost(postElement); - if (fallbackSortAnchor && fallbackSortAnchor.parentElement) { - fallbackSortAnchor.parentElement.insertBefore(marker, fallbackSortAnchor); + if (commentBoundary && commentBoundary.parentElement) { + commentBoundary.parentElement.insertBefore(marker, commentBoundary); setTrackerInsertionMarkerLocked(marker, true); - console.log('[FB Tracker] Post #' + postNum + ' - Marker fallback inserted before comment sort'); + console.log('[FB Tracker] Post #' + postNum + ' - Marker fallback inserted before comment boundary'); return true; } @@ -586,6 +656,25 @@ function positionTrackerInsertionMarker(postElement, marker, buttonBar, postNum return true; } +function isTrackerInsertionMarkerPlacementValid(postElement, marker, buttonBar) { + if (!postElement || !marker || !marker.isConnected || !postElement.contains(marker)) { + return false; + } + + const actionAnchor = getDirectActionAnchorInPost(postElement, buttonBar); + const commentBoundary = findCommentBoundaryAnchorInPost(postElement, actionAnchor); + + if (actionAnchor && isNodeAfter(actionAnchor, marker)) { + return false; + } + + if (commentBoundary && isNodeAfter(marker, commentBoundary)) { + return false; + } + + return true; +} + function ensureTrackerInsertionMarker(postElement, buttonBar, postNum = '?') { if (!postElement) { return null; @@ -607,8 +696,19 @@ function ensureTrackerInsertionMarker(postElement, buttonBar, postNum = '?') { } if (marker.isConnected && postElement.contains(marker) && isTrackerInsertionMarkerLocked(marker)) { - setTrackerAnchorForPost(postElement, marker); - return marker; + if (!isTrackerInsertionMarkerPlacementValid(postElement, marker, buttonBar)) { + setTrackerInsertionMarkerLocked(marker, false); + } else { + setTrackerAnchorForPost(postElement, marker); + return marker; + } + } + + if (marker.isConnected && postElement.contains(marker) && !isTrackerInsertionMarkerLocked(marker)) { + if (isTrackerInsertionMarkerPlacementValid(postElement, marker, buttonBar)) { + setTrackerAnchorForPost(postElement, marker); + return marker; + } } const positioned = positionTrackerInsertionMarker(postElement, marker, buttonBar, postNum); @@ -630,6 +730,19 @@ function insertTrackerAtMarker(marker, trackerElement) { return true; } +function realignExistingTrackerUI(postElement, buttonBar, trackerElement, postNum = '?') { + if (!postElement || !trackerElement || !trackerElement.isConnected) { + return false; + } + + const marker = ensureTrackerInsertionMarker(postElement, buttonBar, postNum); + if (!marker) { + return false; + } + + return insertTrackerAtMarker(marker, trackerElement); +} + const AI_CREDENTIAL_CACHE_TTL = 60 * 1000; // 1 minute cache const aiCredentialCache = { data: null, @@ -3492,10 +3605,7 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options = } if (existingUI) { - const marker = ensureTrackerInsertionMarker(postElement, buttonBar, postNum); - if (marker) { - insertTrackerAtMarker(marker, existingUI); - } + realignExistingTrackerUI(postElement, buttonBar, existingUI, postNum); console.log('[FB Tracker] Post #' + postNum + ' - UI already exists on normalized element'); postElement.setAttribute(PROCESSED_ATTR, '1'); return; @@ -3531,10 +3641,7 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options = postElement.setAttribute(PROCESSED_ATTR, '1'); return; } - const marker = ensureTrackerInsertionMarker(postElement, buttonBar, postNum); - if (marker) { - insertTrackerAtMarker(marker, scopedTracker); - } + realignExistingTrackerUI(postElement, buttonBar, scopedTracker, postNum); console.log('[FB Tracker] Post #' + postNum + ' - Reusing existing tracker UI in same scope'); setTrackerElementForPost(postElement, scopedTracker); postElement.setAttribute(PROCESSED_ATTR, '1'); @@ -4386,6 +4493,7 @@ function findPosts() { if (isInDialog) { if (trackerInSameDialog) { + realignExistingTrackerUI(container, precomputedButtonBar || null, existingTracker, 'existing'); seenSet.add(container); continue; } @@ -4400,6 +4508,12 @@ function findPosts() { } } else { if (alreadyProcessed || (existingTracker && existingTracker.isConnected)) { + if (existingTracker && existingTracker.isConnected) { + realignExistingTrackerUI(container, precomputedButtonBar || null, existingTracker, 'existing'); + setTrackerElementForPost(container, existingTracker); + } else if (alreadyProcessed) { + container.removeAttribute(PROCESSED_ATTR); + } seenSet.add(container); continue; }