diff --git a/.gitignore b/.gitignore index a17ba5e..df3a85f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ backend/data/*.db-shm backend/data/*.db-wal .DS_Store *.log -.env \ No newline at end of file +.env +screenshots/ diff --git a/extension/content.js b/extension/content.js index 1da57a3..b2e4183 100644 --- a/extension/content.js +++ b/extension/content.js @@ -242,6 +242,86 @@ function getTrackerInsertionAnchorInPost(postElement, buttonBar) { return anchor && anchor.parentElement === postElement ? anchor : null; } +function isNodeAfter(node, referenceNode) { + if (!node || !referenceNode || node === referenceNode) { + return false; + } + return Boolean(referenceNode.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING); +} + +function getTopLevelAnchorWithinPost(postElement, element) { + if (!postElement || !element || !postElement.contains(element)) { + return null; + } + + let anchor = element; + while (anchor.parentElement && anchor.parentElement !== postElement) { + anchor = anchor.parentElement; + } + + return anchor && anchor.parentElement === postElement ? anchor : null; +} + +function findCommentSectionAnchorInPost(postElement, actionAnchor = null) { + if (!postElement) { + return null; + } + + const selectors = [ + '[aria-label*="relevantes zuerst" i]', + '[aria-label*="most relevant" i]', + '[aria-label*="neueste" i]', + '[aria-label*="newest" i]', + '[aria-label*="kommentare" i]', + '[aria-label*="comments" i]', + '[aria-roledescription*="kommentar" i]', + '[aria-roledescription*="comment" i]', + '[data-testid*="comment" i]', + '[data-scope="comment"]', + 'div[role="textbox"][contenteditable="true"]', + 'div[contenteditable="true"][data-lexical-editor="true"]', + '[aria-placeholder*="komment" i]', + '[aria-placeholder*="comment" i]' + ]; + + const anchors = new Set(); + + postElement.querySelectorAll(selectors.join(', ')).forEach((candidate) => { + if (!candidate || candidate.closest('.fb-tracker-ui')) { + return; + } + + let anchorSeed = candidate; + const isTextBox = candidate.matches('div[role="textbox"][contenteditable="true"], div[contenteditable="true"][data-lexical-editor="true"]'); + if (isTextBox) { + anchorSeed = candidate.closest('form[role="presentation"]') + || candidate.closest('form') + || candidate; + } + + const anchor = getTopLevelAnchorWithinPost(postElement, anchorSeed); + if (!anchor || anchor === actionAnchor) { + return; + } + if (actionAnchor && !isNodeAfter(anchor, actionAnchor)) { + return; + } + + anchors.add(anchor); + }); + + if (!anchors.size) { + return null; + } + + return Array.from(anchors).sort((a, b) => { + if (a === b) { + return 0; + } + return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1; + })[0]; +} + const AI_CREDENTIAL_CACHE_TTL = 60 * 1000; // 1 minute cache const aiCredentialCache = { data: null, @@ -3790,29 +3870,35 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options = inserted = tryInsertBeforeReelsCommentComposer(); } - // 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; - } + const actionAnchor = getTrackerInsertionAnchorInPost(postElement, buttonBar); + const commentAnchor = findCommentSectionAnchorInPost(postElement, actionAnchor); + + // Strategy 1: Insert before comment section so the bar stays above lazy-loaded comments + if (!inserted && commentAnchor && commentAnchor.parentElement) { + commentAnchor.parentElement.insertBefore(container, commentAnchor); + console.log('[FB Tracker] Post #' + postNum + ' - UI inserted before comment section. ID: #' + container.id); + inserted = true; } - // Strategy 2: Insert directly after the button bar if still inside the post container + // Strategy 2: Insert right after the action block inside the current post container + if (!inserted && 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 3: 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 + // Strategy 4: 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 external button bar parent (legacy). ID: #' + container.id); inserted = true; } - // Strategy 4: Append to post element + // Strategy 5: Append to post element if (!inserted) { postElement.appendChild(container); console.log('[FB Tracker] Post #' + postNum + ' - UI inserted into article (fallback). ID: #' + container.id); @@ -3843,9 +3929,12 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options = console.log('[FB Tracker] Post #' + postNum + ' - UI was removed, re-inserting...'); observer.disconnect(); // Try to re-insert - const actionAnchor = getTrackerInsertionAnchorInPost(postElement, buttonBar); - if (actionAnchor && actionAnchor.parentElement) { - actionAnchor.parentElement.insertBefore(container, actionAnchor.nextSibling); + const currentActionAnchor = getTrackerInsertionAnchorInPost(postElement, buttonBar); + const currentCommentAnchor = findCommentSectionAnchorInPost(postElement, currentActionAnchor); + if (currentCommentAnchor && currentCommentAnchor.parentElement) { + currentCommentAnchor.parentElement.insertBefore(container, currentCommentAnchor); + } else if (currentActionAnchor && currentActionAnchor.parentElement) { + currentActionAnchor.parentElement.insertBefore(container, currentActionAnchor.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)) {