diff --git a/post_feed.js b/post_feed.js index ccc712e..8b52536 100644 --- a/post_feed.js +++ b/post_feed.js @@ -4,7 +4,6 @@ (function () { 'use strict'; - const SERVER_URL = 'https://cv.intaleqapp.com/cv/server/generate_cv.php'; const BUTTON_CLASS = 'lja-comment-btn'; const BOX_CLASS = 'lja-comment-box'; @@ -17,8 +16,8 @@ // ─── Extract post text from a feed item ────────────────────────────────── function extractPostText(postEl) { - // Try multiple selectors LinkedIn uses const selectors = [ + '[data-testid="expandable-text-box"]', // New LinkedIn UI '.feed-shared-update-v2__description .break-words span[dir]', '.feed-shared-text-view span[dir]', '.update-components-text span[dir]', @@ -27,8 +26,10 @@ ]; for (const sel of selectors) { const el = postEl.querySelector(sel); - if (el && el.textContent.trim().length > 20) { - return el.textContent.trim(); + if (el) { + // Strip out the "... more" text if it exists + const text = el.textContent.replace(/…\s*more/gi, '').trim(); + if (text.length > 20) return text; } } return null; @@ -45,6 +46,15 @@ const el = postEl.querySelector(sel); if (el) return el; } + + // New UI fallback: find the comment button SVG and get its parent container + const commentSvg = postEl.querySelector('svg#comment-small, svg[data-test-icon="comment-small"]'); + if (commentSvg) { + const btn = commentSvg.closest('button'); + if (btn && btn.parentNode) { + return btn.parentNode; // This is the action bar container + } + } return null; } @@ -54,10 +64,11 @@ '.comments-comment-box__form-container .ql-editor', '.comments-comment-texteditor .ql-editor', '.ql-editor[contenteditable="true"]', + '[contenteditable="true"][role="textbox"]', + 'div[contenteditable="true"]' ]; - // Search inside the post first, then fall back to document for (const sel of selectors) { - const el = postEl.querySelector(sel) || document.querySelector(sel); + const el = postEl.querySelector(sel); if (el) return el; } return null; @@ -66,18 +77,14 @@ // ─── Fill comment input (handles LinkedIn's Quill editor) ──────────────── function fillCommentInput(inputEl, text) { inputEl.focus(); - // Clear existing content inputEl.innerHTML = ''; - // Use execCommand for rich text editors (most reliable cross-browser) document.execCommand('insertText', false, text); - // Fallback: dispatch input event for React to pick up inputEl.dispatchEvent(new InputEvent('input', { bubbles: true, data: text })); inputEl.dispatchEvent(new Event('change', { bubbles: true })); } // ─── Create the comment suggestion box ─────────────────────────────────── function createCommentBox(postEl, commentText) { - // Remove any existing box on this post const existing = postEl.querySelector('.' + BOX_CLASS); if (existing) existing.remove(); @@ -103,10 +110,8 @@ `; - // Close box.querySelector('.lja-cb-close').addEventListener('click', () => box.remove()); - // Copy to clipboard box.querySelector('.lja-cb-copy').addEventListener('click', function () { const text = box.querySelector('.lja-cb-text').value; navigator.clipboard.writeText(text).then(() => { @@ -115,15 +120,17 @@ }); }); - // Paste into LinkedIn comment field box.querySelector('.lja-cb-paste').addEventListener('click', function () { const text = box.querySelector('.lja-cb-text').value; - // First click the LinkedIn comment button to open the field if not open const commentTrigger = postEl.querySelector( - '.comment-button, [aria-label*="comment" i], [data-control-name="comment"]' + '.comment-button, [aria-label*="comment" i], [data-control-name="comment"], svg#comment-small' ); - if (commentTrigger) commentTrigger.click(); + let btnToClick = commentTrigger; + if (btnToClick && btnToClick.tagName.toLowerCase() === 'svg') { + btnToClick = btnToClick.closest('button') || btnToClick; + } + if (btnToClick) btnToClick.click(); setTimeout(() => { const inputEl = findCommentInput(postEl); @@ -132,20 +139,17 @@ this.textContent = '✅ Done!'; setTimeout(() => { this.textContent = '✅ Paste into Comment'; }, 1500); } else { - // Fallback: copy to clipboard and tell user navigator.clipboard.writeText(text); this.textContent = '📋 Copied! Paste manually'; } - }, 300); + }, 500); }); - // Regenerate box.querySelector('.lja-cb-regen').addEventListener('click', async function () { box.remove(); await generateComment(postEl); }); - // Insert before the action bar const actionBar = findActionBar(postEl); if (actionBar) { actionBar.parentNode.insertBefore(box, actionBar.nextSibling); @@ -168,7 +172,6 @@ return; } - // Show spinner on the button const btn = postEl.querySelector('.' + BUTTON_CLASS); if (btn) { btn.disabled = true; @@ -206,6 +209,10 @@ // ─── Inject button into a single post ──────────────────────────────────── function injectButton(postEl) { if (postEl.querySelector('.' + BUTTON_CLASS)) return; // Already injected + + const actionBar = findActionBar(postEl); + if (!actionBar) return; + if (!extractPostText(postEl)) return; // Skip non-text posts silently const btn = document.createElement('button'); @@ -215,43 +222,31 @@ btn.addEventListener('click', (e) => { e.stopPropagation(); + e.preventDefault(); generateComment(postEl); }); - const actionBar = findActionBar(postEl); - if (actionBar) { - actionBar.appendChild(btn); - } + actionBar.appendChild(btn); } // ─── Process all posts on the page ─────────────────────────────────────── function processAllPosts() { + // Check both classic LinkedIn UI and the new React/hash-class UI const posts = document.querySelectorAll( - '.feed-shared-update-v2, .occludable-update, [data-urn*="activity"]' + '.feed-shared-update-v2, .occludable-update, [data-urn*="activity"], [role="listitem"]' ); posts.forEach(injectButton); } // ─── MutationObserver: watch for new posts (infinite scroll) ───────────── - const observer = new MutationObserver((mutations) => { - for (const mutation of mutations) { - for (const node of mutation.addedNodes) { - if (node.nodeType !== Node.ELEMENT_NODE) continue; - // Check if the node itself is a post - if ( - node.classList?.contains('feed-shared-update-v2') || - node.classList?.contains('occludable-update') || - node.dataset?.urn?.includes('activity') - ) { - injectButton(node); - } - // Or if it contains posts - const innerPosts = node.querySelectorAll?.( - '.feed-shared-update-v2, .occludable-update, [data-urn*="activity"]' - ); - innerPosts?.forEach(injectButton); - } - } + let observerTimer; + const observer = new MutationObserver(() => { + // Throttle the DOM scanning to prevent heavy performance hits on React updates + if (observerTimer) return; + observerTimer = setTimeout(() => { + processAllPosts(); + observerTimer = null; + }, 500); }); // ─── Initialize ────────────────────────────────────────────────────────── @@ -260,12 +255,10 @@ observer.observe(document.body, { childList: true, subtree: true }); } - // Wait for DOM to be ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { - // Run immediately, then again after a short delay for LinkedIn's SPA init(); - setTimeout(init, 2000); + setTimeout(init, 2000); // Fallback for delayed SPA renders } })();