277 lines
9.7 KiB
JavaScript
277 lines
9.7 KiB
JavaScript
// post_feed.js — LinkedIn Feed Smart Comment Generator
|
|
// Operates ONLY on linkedin.com/feed pages — fully independent from content.js
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
const BUTTON_CLASS = 'lja-comment-btn';
|
|
const BOX_CLASS = 'lja-comment-box';
|
|
|
|
// ─── Utility: get stored settings ────────────────────────────────────────
|
|
function getSettings() {
|
|
return new Promise(resolve => {
|
|
if (!chrome || !chrome.storage) {
|
|
resolve({});
|
|
return;
|
|
}
|
|
chrome.storage.sync.get(['apiKey', 'language'], (syncData) => {
|
|
if (syncData && syncData.apiKey) {
|
|
resolve(syncData);
|
|
} else {
|
|
chrome.storage.local.get(['apiKey', 'language'], (localData) => {
|
|
resolve(localData || {});
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// ─── Extract post text from a feed item ──────────────────────────────────
|
|
function extractPostText(postEl) {
|
|
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]',
|
|
'.feed-shared-update-v2__description',
|
|
'.feed-shared-text',
|
|
];
|
|
for (const sel of selectors) {
|
|
const el = postEl.querySelector(sel);
|
|
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;
|
|
}
|
|
|
|
// ─── Find the action bar in a post ───────────────────────────────────────
|
|
function findActionBar(postEl) {
|
|
const selectors = [
|
|
'.feed-shared-social-action-bar',
|
|
'.social-actions-bar',
|
|
'[data-urn] .feed-shared-social-action-bar',
|
|
];
|
|
for (const sel of selectors) {
|
|
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;
|
|
}
|
|
|
|
// ─── Find comment input field ─────────────────────────────────────────────
|
|
function findCommentInput(postEl) {
|
|
const selectors = [
|
|
'.comments-comment-box__form-container .ql-editor',
|
|
'.comments-comment-texteditor .ql-editor',
|
|
'.ql-editor[contenteditable="true"]',
|
|
'[contenteditable="true"][role="textbox"]',
|
|
'div[contenteditable="true"]'
|
|
];
|
|
for (const sel of selectors) {
|
|
const el = postEl.querySelector(sel);
|
|
if (el) return el;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ─── Fill comment input (handles LinkedIn's Quill editor) ────────────────
|
|
function fillCommentInput(inputEl, text) {
|
|
inputEl.focus();
|
|
inputEl.innerHTML = '';
|
|
document.execCommand('insertText', false, text);
|
|
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) {
|
|
const existing = postEl.querySelector('.' + BOX_CLASS);
|
|
if (existing) existing.remove();
|
|
|
|
const isRTL = /[\u0600-\u06FF]/.test(commentText);
|
|
|
|
const box = document.createElement('div');
|
|
box.className = BOX_CLASS;
|
|
box.innerHTML = `
|
|
<div class="lja-cb-header">
|
|
<span class="lja-cb-icon">🤖</span>
|
|
<span class="lja-cb-title">Smart Comment</span>
|
|
<button class="lja-cb-close" title="Close">✕</button>
|
|
</div>
|
|
<textarea
|
|
class="lja-cb-text"
|
|
dir="${isRTL ? 'rtl' : 'ltr'}"
|
|
style="text-align: ${isRTL ? 'right' : 'left'}"
|
|
>${commentText}</textarea>
|
|
<div class="lja-cb-actions">
|
|
<button class="lja-cb-copy">📋 Copy</button>
|
|
<button class="lja-cb-paste">✅ Paste into Comment</button>
|
|
<button class="lja-cb-regen">🔄 Regenerate</button>
|
|
</div>
|
|
`;
|
|
|
|
box.querySelector('.lja-cb-close').addEventListener('click', () => box.remove());
|
|
|
|
box.querySelector('.lja-cb-copy').addEventListener('click', function () {
|
|
const text = box.querySelector('.lja-cb-text').value;
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
this.textContent = '✅ Copied!';
|
|
setTimeout(() => { this.textContent = '📋 Copy'; }, 1500);
|
|
});
|
|
});
|
|
|
|
box.querySelector('.lja-cb-paste').addEventListener('click', function () {
|
|
const text = box.querySelector('.lja-cb-text').value;
|
|
|
|
const commentTrigger = postEl.querySelector(
|
|
'.comment-button, [aria-label*="comment" i], [data-control-name="comment"], svg#comment-small'
|
|
);
|
|
let btnToClick = commentTrigger;
|
|
if (btnToClick && btnToClick.tagName.toLowerCase() === 'svg') {
|
|
btnToClick = btnToClick.closest('button') || btnToClick;
|
|
}
|
|
if (btnToClick) btnToClick.click();
|
|
|
|
setTimeout(() => {
|
|
const inputEl = findCommentInput(postEl);
|
|
if (inputEl) {
|
|
fillCommentInput(inputEl, text);
|
|
this.textContent = '✅ Done!';
|
|
setTimeout(() => { this.textContent = '✅ Paste into Comment'; }, 1500);
|
|
} else {
|
|
navigator.clipboard.writeText(text);
|
|
this.textContent = '📋 Copied! Paste manually';
|
|
}
|
|
}, 500);
|
|
});
|
|
|
|
box.querySelector('.lja-cb-regen').addEventListener('click', async function () {
|
|
box.remove();
|
|
await generateComment(postEl);
|
|
});
|
|
|
|
const actionBar = findActionBar(postEl);
|
|
if (actionBar) {
|
|
actionBar.parentNode.insertBefore(box, actionBar.nextSibling);
|
|
} else {
|
|
postEl.appendChild(box);
|
|
}
|
|
}
|
|
|
|
// ─── Core: call server and generate comment ───────────────────────────────
|
|
async function generateComment(postEl) {
|
|
const settings = await getSettings();
|
|
if (!settings || !settings.apiKey) {
|
|
alert('Please set your Gemini API key in the extension popup first.\n(Debug: Key not found in storage)');
|
|
return;
|
|
}
|
|
|
|
const postText = extractPostText(postEl);
|
|
if (!postText) {
|
|
alert('Could not read post text. The post might be an image or video.');
|
|
return;
|
|
}
|
|
|
|
const btn = postEl.querySelector('.' + BUTTON_CLASS);
|
|
if (btn) {
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="lja-spinner"></span> Thinking...';
|
|
}
|
|
|
|
try {
|
|
const response = await chrome.runtime.sendMessage({
|
|
type: 'GEMINI_REQUEST',
|
|
payload: {
|
|
apiKey: settings.apiKey,
|
|
action: 'generateComment',
|
|
postText: postText
|
|
}
|
|
});
|
|
|
|
if (!response.success) {
|
|
throw new Error(response.error || 'Unknown error');
|
|
}
|
|
|
|
const commentText = response.data.comment || response.data;
|
|
createCommentBox(postEl, commentText);
|
|
|
|
} catch (e) {
|
|
console.error('[LJA Feed]', e);
|
|
alert('Comment generation failed: ' + e.message);
|
|
} finally {
|
|
if (btn) {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '💬 Smart Comment';
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── 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');
|
|
btn.className = BUTTON_CLASS;
|
|
btn.innerHTML = '💬 Smart Comment';
|
|
btn.title = 'Generate an AI-powered comment for this post';
|
|
|
|
btn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
generateComment(postEl);
|
|
});
|
|
|
|
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"], [role="listitem"]'
|
|
);
|
|
posts.forEach(injectButton);
|
|
}
|
|
|
|
// ─── MutationObserver: watch for new posts (infinite scroll) ─────────────
|
|
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 ──────────────────────────────────────────────────────────
|
|
function init() {
|
|
processAllPosts();
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
setTimeout(init, 2000); // Fallback for delayed SPA renders
|
|
}
|
|
})();
|