Size
15.5 KB
Version
1.0.1
Created
Nov 28, 2025
Updated
15 days ago
1// ==UserScript==
2// @name Facebook Comment Saver
3// @description Save and manage Facebook comments for later reference
4// @version 1.0.1
5// @match https://*.facebook.com/*
6// @icon https://static.xx.fbcdn.net/rsrc.php/y1/r/ay1hV6OlegS.ico?_nc_eui2=AeEiD2vGYDgZiQQ_33AGzOMUDLqwI5i5CZMMurAjmLkJkxJRNI1Qr4eZ28h8TKPkusBJEkFTlp8LjCOrNIMOpv2k
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('Facebook Comment Saver extension loaded');
12
13 // Debounce function to prevent excessive calls
14 function debounce(func, wait) {
15 let timeout;
16 return function executedFunction(...args) {
17 const later = () => {
18 clearTimeout(timeout);
19 func(...args);
20 };
21 clearTimeout(timeout);
22 timeout = setTimeout(later, wait);
23 };
24 }
25
26 // Add custom styles for the save buttons and panel
27 TM_addStyle(`
28 .fb-comment-save-btn {
29 background: #1877f2;
30 color: white;
31 border: none;
32 padding: 6px 12px;
33 border-radius: 6px;
34 cursor: pointer;
35 font-size: 13px;
36 font-weight: 600;
37 margin-left: 8px;
38 transition: background 0.2s;
39 }
40 .fb-comment-save-btn:hover {
41 background: #166fe5;
42 }
43 .fb-comment-save-btn.saved {
44 background: #42b72a;
45 }
46 .fb-saved-comments-panel {
47 position: fixed;
48 top: 60px;
49 right: 20px;
50 width: 400px;
51 max-height: 80vh;
52 background: white;
53 border-radius: 8px;
54 box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
55 z-index: 9999;
56 display: none;
57 flex-direction: column;
58 }
59 .fb-saved-comments-panel.show {
60 display: flex;
61 }
62 .fb-panel-header {
63 padding: 16px;
64 border-bottom: 1px solid #e4e6eb;
65 display: flex;
66 justify-content: space-between;
67 align-items: center;
68 background: #f0f2f5;
69 border-radius: 8px 8px 0 0;
70 }
71 .fb-panel-header h3 {
72 margin: 0;
73 font-size: 17px;
74 font-weight: 600;
75 color: #050505;
76 }
77 .fb-panel-close {
78 background: none;
79 border: none;
80 font-size: 24px;
81 cursor: pointer;
82 color: #65676b;
83 padding: 0;
84 width: 36px;
85 height: 36px;
86 border-radius: 50%;
87 display: flex;
88 align-items: center;
89 justify-content: center;
90 }
91 .fb-panel-close:hover {
92 background: #e4e6eb;
93 }
94 .fb-panel-content {
95 padding: 16px;
96 overflow-y: auto;
97 flex: 1;
98 }
99 .fb-saved-comment-item {
100 background: #f0f2f5;
101 padding: 12px;
102 border-radius: 8px;
103 margin-bottom: 12px;
104 }
105 .fb-saved-comment-author {
106 font-weight: 600;
107 color: #050505;
108 margin-bottom: 4px;
109 }
110 .fb-saved-comment-text {
111 color: #050505;
112 margin-bottom: 8px;
113 line-height: 1.4;
114 }
115 .fb-saved-comment-meta {
116 font-size: 12px;
117 color: #65676b;
118 margin-bottom: 8px;
119 }
120 .fb-saved-comment-actions {
121 display: flex;
122 gap: 8px;
123 }
124 .fb-comment-action-btn {
125 background: #e4e6eb;
126 border: none;
127 padding: 6px 12px;
128 border-radius: 6px;
129 cursor: pointer;
130 font-size: 12px;
131 font-weight: 600;
132 color: #050505;
133 }
134 .fb-comment-action-btn:hover {
135 background: #d8dadf;
136 }
137 .fb-comment-action-btn.delete {
138 background: #f02849;
139 color: white;
140 }
141 .fb-comment-action-btn.delete:hover {
142 background: #d02343;
143 }
144 .fb-view-saved-btn {
145 position: fixed;
146 bottom: 20px;
147 right: 20px;
148 background: #1877f2;
149 color: white;
150 border: none;
151 padding: 12px 20px;
152 border-radius: 24px;
153 cursor: pointer;
154 font-size: 14px;
155 font-weight: 600;
156 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
157 z-index: 9998;
158 display: flex;
159 align-items: center;
160 gap: 8px;
161 }
162 .fb-view-saved-btn:hover {
163 background: #166fe5;
164 }
165 .fb-saved-count {
166 background: #42b72a;
167 padding: 2px 8px;
168 border-radius: 12px;
169 font-size: 12px;
170 }
171 .fb-empty-state {
172 text-align: center;
173 padding: 40px 20px;
174 color: #65676b;
175 }
176 `);
177
178 // Function to extract comment data
179 function extractCommentData(commentElement) {
180 try {
181 // Find author name
182 const authorElement = commentElement.querySelector('a[role="link"] span');
183 const author = authorElement ? authorElement.textContent.trim() : 'Unknown User';
184
185 // Find comment text - try multiple selectors
186 let commentText = '';
187 const textSelectors = [
188 'div[dir="auto"][style*="text-align"]',
189 'div.x193iq5w span',
190 'div[data-ad-comet-preview="message"] span'
191 ];
192
193 for (const selector of textSelectors) {
194 const textElement = commentElement.querySelector(selector);
195 if (textElement && textElement.textContent.trim()) {
196 commentText = textElement.textContent.trim();
197 break;
198 }
199 }
200
201 // Find timestamp
202 const timeElement = commentElement.querySelector('a[href*="comment_id"] span');
203 const timestamp = timeElement ? timeElement.textContent.trim() : new Date().toLocaleString();
204
205 // Find post URL
206 const postLink = commentElement.querySelector('a[href*="comment_id"]');
207 const postUrl = postLink ? postLink.href : window.location.href;
208
209 return {
210 author,
211 text: commentText,
212 timestamp,
213 postUrl,
214 savedAt: new Date().toISOString()
215 };
216 } catch (error) {
217 console.error('Error extracting comment data:', error);
218 return null;
219 }
220 }
221
222 // Function to save a comment
223 async function saveComment(commentData) {
224 try {
225 const savedComments = await GM.getValue('savedComments', []);
226
227 // Generate unique ID
228 const commentId = Date.now() + '_' + Math.random().toString(36).substr(2, 9);
229 commentData.id = commentId;
230
231 savedComments.push(commentData);
232 await GM.setValue('savedComments', savedComments);
233
234 console.log('Comment saved successfully:', commentData);
235 updateSavedCount();
236 return true;
237 } catch (error) {
238 console.error('Error saving comment:', error);
239 return false;
240 }
241 }
242
243 // Function to delete a saved comment
244 async function deleteComment(commentId) {
245 try {
246 const savedComments = await GM.getValue('savedComments', []);
247 const filteredComments = savedComments.filter(c => c.id !== commentId);
248 await GM.setValue('savedComments', filteredComments);
249 console.log('Comment deleted successfully');
250 updateSavedCount();
251 return true;
252 } catch (error) {
253 console.error('Error deleting comment:', error);
254 return false;
255 }
256 }
257
258 // Function to add save button to comments
259 function addSaveButtonToComment(commentElement) {
260 // Check if button already exists
261 if (commentElement.querySelector('.fb-comment-save-btn')) {
262 return;
263 }
264
265 // Find the action bar (like, reply buttons area)
266 const actionBar = commentElement.querySelector('div[role="toolbar"], div[aria-label*="actions"]');
267 if (!actionBar) {
268 return;
269 }
270
271 // Create save button
272 const saveBtn = document.createElement('button');
273 saveBtn.className = 'fb-comment-save-btn';
274 saveBtn.textContent = 'š¾ Save';
275 saveBtn.title = 'Save this comment';
276
277 saveBtn.addEventListener('click', async (e) => {
278 e.preventDefault();
279 e.stopPropagation();
280
281 const commentData = extractCommentData(commentElement);
282 if (commentData && commentData.text) {
283 const success = await saveComment(commentData);
284 if (success) {
285 saveBtn.textContent = 'ā Saved';
286 saveBtn.classList.add('saved');
287 setTimeout(() => {
288 saveBtn.textContent = 'š¾ Save';
289 saveBtn.classList.remove('saved');
290 }, 2000);
291 }
292 } else {
293 alert('Could not extract comment data. Please try again.');
294 }
295 });
296
297 actionBar.appendChild(saveBtn);
298 }
299
300 // Function to update saved count badge
301 async function updateSavedCount() {
302 const savedComments = await GM.getValue('savedComments', []);
303 const countBadge = document.querySelector('.fb-saved-count');
304 if (countBadge) {
305 countBadge.textContent = savedComments.length;
306 }
307 }
308
309 // Function to render saved comments panel
310 async function renderSavedCommentsPanel() {
311 const savedComments = await GM.getValue('savedComments', []);
312 const panel = document.querySelector('.fb-saved-comments-panel');
313 const content = panel.querySelector('.fb-panel-content');
314
315 if (savedComments.length === 0) {
316 content.innerHTML = '<div class="fb-empty-state">No saved comments yet.<br>Click the "š¾ Save" button on any comment to save it.</div>';
317 return;
318 }
319
320 content.innerHTML = savedComments.map(comment => `
321 <div class="fb-saved-comment-item" data-comment-id="${comment.id}">
322 <div class="fb-saved-comment-author">${comment.author}</div>
323 <div class="fb-saved-comment-text">${comment.text}</div>
324 <div class="fb-saved-comment-meta">
325 Saved: ${new Date(comment.savedAt).toLocaleString()}<br>
326 Original: ${comment.timestamp}
327 </div>
328 <div class="fb-saved-comment-actions">
329 <button class="fb-comment-action-btn view-post" data-url="${comment.postUrl}">View Post</button>
330 <button class="fb-comment-action-btn copy-text">Copy Text</button>
331 <button class="fb-comment-action-btn delete">Delete</button>
332 </div>
333 </div>
334 `).join('');
335
336 // Add event listeners to action buttons
337 content.querySelectorAll('.view-post').forEach(btn => {
338 btn.addEventListener('click', () => {
339 const url = btn.getAttribute('data-url');
340 window.open(url, '_blank');
341 });
342 });
343
344 content.querySelectorAll('.copy-text').forEach(btn => {
345 btn.addEventListener('click', async () => {
346 const commentItem = btn.closest('.fb-saved-comment-item');
347 const text = commentItem.querySelector('.fb-saved-comment-text').textContent;
348 await GM.setClipboard(text);
349 btn.textContent = 'Copied!';
350 setTimeout(() => {
351 btn.textContent = 'Copy Text';
352 }, 1500);
353 });
354 });
355
356 content.querySelectorAll('.delete').forEach(btn => {
357 btn.addEventListener('click', async () => {
358 const commentItem = btn.closest('.fb-saved-comment-item');
359 const commentId = commentItem.getAttribute('data-comment-id');
360 if (confirm('Are you sure you want to delete this saved comment?')) {
361 await deleteComment(commentId);
362 await renderSavedCommentsPanel();
363 }
364 });
365 });
366 }
367
368 // Function to create the saved comments panel
369 function createSavedCommentsPanel() {
370 const panel = document.createElement('div');
371 panel.className = 'fb-saved-comments-panel';
372 panel.innerHTML = `
373 <div class="fb-panel-header">
374 <h3>Saved Comments</h3>
375 <button class="fb-panel-close">Ć</button>
376 </div>
377 <div class="fb-panel-content"></div>
378 `;
379
380 document.body.appendChild(panel);
381
382 // Close button
383 panel.querySelector('.fb-panel-close').addEventListener('click', () => {
384 panel.classList.remove('show');
385 });
386
387 return panel;
388 }
389
390 // Function to create the view saved button
391 async function createViewSavedButton() {
392 const savedComments = await GM.getValue('savedComments', []);
393
394 const btn = document.createElement('button');
395 btn.className = 'fb-view-saved-btn';
396 btn.innerHTML = `
397 š Saved Comments
398 <span class="fb-saved-count">${savedComments.length}</span>
399 `;
400
401 btn.addEventListener('click', async () => {
402 const panel = document.querySelector('.fb-saved-comments-panel');
403 if (panel.classList.contains('show')) {
404 panel.classList.remove('show');
405 } else {
406 await renderSavedCommentsPanel();
407 panel.classList.add('show');
408 }
409 });
410
411 document.body.appendChild(btn);
412 }
413
414 // Function to scan and add buttons to all comments
415 function scanForComments() {
416 // Find all comment elements - Facebook uses various selectors
417 const commentSelectors = [
418 'div[role="article"]',
419 'div[aria-label*="Comment"]'
420 ];
421
422 const allComments = new Set();
423 commentSelectors.forEach(selector => {
424 document.querySelectorAll(selector).forEach(el => {
425 // Check if it looks like a comment (has text content and action buttons)
426 if (el.querySelector('div[role="toolbar"]') || el.querySelector('div[aria-label*="actions"]')) {
427 allComments.add(el);
428 }
429 });
430 });
431
432 allComments.forEach(comment => {
433 addSaveButtonToComment(comment);
434 });
435
436 console.log(`Scanned and found ${allComments.size} comments`);
437 }
438
439 // Debounced scan function
440 const debouncedScan = debounce(scanForComments, 1000);
441
442 // Initialize the extension
443 async function init() {
444 console.log('Initializing Facebook Comment Saver...');
445
446 // Wait for body to be ready
447 TM_runBody(() => {
448 // Create UI elements
449 createSavedCommentsPanel();
450 createViewSavedButton();
451
452 // Initial scan
453 setTimeout(scanForComments, 2000);
454
455 // Set up MutationObserver to detect new comments
456 const observer = new MutationObserver(debouncedScan);
457 observer.observe(document.body, {
458 childList: true,
459 subtree: true
460 });
461
462 console.log('Facebook Comment Saver initialized successfully');
463 });
464 }
465
466 // Start the extension
467 init();
468})();