Auto-deploy: 2026-05-18 01:59:28

This commit is contained in:
Hamza-Ayed
2026-05-18 01:59:28 +03:00
parent 42aec81504
commit c0465e4ee4

View File

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