commit 48839bca789d1acda42240232a45d9cd9d8ad905 Author: James Guan Date: Tue Sep 15 21:41:27 2026 -0400 Add tool to clean up facebook diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ecad9db --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# macOS +.DS_Store +.AppleDouble +.LSOverride +Icon? +._* + +# Editor / IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Logs & debug +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Dependencies (if added later) +node_modules/ + +# Env / secrets +.env +.env.* +!.env.example + +# Chrome extension packaging +*.crx +*.pem +*.zip +web-ext-artifacts/ + +# OS / misc +Thumbs.db +Desktop.ini diff --git a/README.md b/README.md new file mode 100644 index 0000000..c89d4f9 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# FB Activity Cleaner + +Chrome extension that cleans your Facebook **Activity Log** while you stay logged in. + +## What it does + +**Comments page** +- Deletes comments one-by-one (More options → Delete) +- Skips Tesla / Ford / Pokémon community items by default +- **Always deletes offensive comments**, even if they are on a skipped community page + +**Posts / photos / videos page** +- Moves shares and status updates to trash +- Keeps profile pictures, cover photos, and photo uploads + +## Install (Chrome / Edge / Brave) + +1. Open `chrome://extensions` +2. Enable **Developer mode** +3. Click **Load unpacked** +4. Select this folder: `facebook-activity-cleaner` +5. Open Facebook → Activity log → Comments (or Posts) with **manage mode** on +6. Use the floating **FB Activity Cleaner** panel → **Start** + +## Useful URLs + +Replace with your profile if needed: + +``` +https://www.facebook.com/me/allactivity?category_key=COMMENTSCLUSTER&manage_mode=true +https://www.facebook.com/me/allactivity?category_key=MANAGEPOSTSPHOTOSANDVIDEOS&manage_mode=true +``` + +## Notes + +- Meta rate-limits bulk actions; the extension pauses ~90s on “Try again later” +- Skip / offensive phrase lists are editable in the panel and saved locally +- Run it on the Activity Log manage-mode pages only +- Review skips before you walk away — community names must appear in the row text/label to be skipped diff --git a/content.js b/content.js new file mode 100644 index 0000000..93a2b21 --- /dev/null +++ b/content.js @@ -0,0 +1,620 @@ +(() => { + if (window.__fbActivityCleanerLoaded) return; + window.__fbActivityCleanerLoaded = true; + + const DEFAULT_SKIP = + "tesla, ford, pokemon, pokémon, pika, pokejohn, pokemongo, pokemon go"; + const DEFAULT_OFFENSIVE = + "kill yourself, kys, rape, nigger, nigga, faggot, retard, whore, slut, cunt, " + + "go die, hope you die, piece of shit, fucking idiot, dumb bitch, dumbass, " + + "get railed, shitty people, get fucked"; + + const KEEP_POST_RE = + /profile picture|cover photo|added \d+ new photo|added a new photo|uploaded a photo|uploaded \d+ photos|posted a photo|posted \d+ photos/i; + + const state = { + running: false, + mode: "comments", // comments | posts + deleted: 0, + skipped: 0, + offensiveForced: 0, + errors: 0, + lastAction: "Idle", + delayMs: 2500, + failStreak: 0, + }; + + let stopFlag = false; + const countedSkipLabels = new Set(); + + function sleep(ms) { + return new Promise((r) => setTimeout(r, ms)); + } + + function parseList(text) { + return String(text || "") + .split(/[\n,]+/) + .map((s) => s.trim().toLowerCase()) + .filter(Boolean); + } + + function escapeRegExp(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + } + + function matchesAny(haystack, terms) { + const h = haystack || ""; + return terms.some((t) => { + if (!t) return false; + try { + const re = new RegExp(`(^|[^a-z0-9])${escapeRegExp(t)}([^a-z0-9]|$)`, "i"); + return re.test(h); + } catch { + return h.toLowerCase().includes(t.toLowerCase()); + } + }); + } + + function pageText() { + return document.body?.innerText || ""; + } + + function isActivityLog() { + return /\/allactivity/.test(location.pathname + location.search) || + pageText().includes("Activity log"); + } + + function detectMode() { + const q = location.search; + if (/MANAGEPOSTSPHOTOSANDVIDEOS/i.test(q)) return "posts"; + if (/COMMENTSCLUSTER/i.test(q)) return "comments"; + const h = pageText(); + if (/Your posts, photos and videos/i.test(h)) return "posts"; + if (/\bComments\b/.test(h) && /Activity log/i.test(h)) return "comments"; + return state.mode; + } + + function moreButtons() { + // Never stamp dataset flags on these nodes — Facebook/React reuses them + // after a delete, which made us skip every other item. + return Array.from( + document.querySelectorAll('[aria-label^="More options"]') + ); + } + + /** Tight row text only — walking too far up pulls in the whole list. */ + function rowRoot(btn) { + return ( + btn.closest('[role="article"]') || + btn.closest('[role="listitem"]') || + btn.closest("li") || + btn.parentElement?.parentElement?.parentElement || + btn.parentElement + ); + } + + function rowText(btn) { + const label = btn.getAttribute("aria-label") || ""; + const root = rowRoot(btn); + const block = (root && root.innerText) || ""; + return `${label}\n${block.slice(0, 500)}`; + } + + function robustClick(el) { + if (!el) return false; + try { + // Single click only — double-firing confuses Facebook and triggers errors + if (typeof el.click === "function") el.click(); + else { + el.dispatchEvent( + new MouseEvent("click", { bubbles: true, cancelable: true, view: window }) + ); + } + return true; + } catch { + return false; + } + } + + function clickText(text, root = document) { + const els = Array.from( + root.querySelectorAll('[role="button"], button, [role="menuitem"], span') + ); + const el = els.find((e) => (e.innerText || "").trim() === text); + if (el) { + robustClick(el); + return true; + } + return false; + } + + function hasFbError() { + const t = pageText(); + return ( + /Something went wrong/i.test(t) || + /Try again later/i.test(t) + ); + } + + function isProcessing() { + const t = pageText(); + return /still processing your changes/i.test(t) || + /We're still processing/i.test(t); + } + + function dismissErrorDialogs() { + let dismissed = false; + const dialogs = Array.from(document.querySelectorAll('[role="dialog"]')); + for (const d of dialogs) { + const txt = d.innerText || ""; + if ( + /Something went wrong|Please try again|Try again later/i.test(txt) + ) { + const ok = + Array.from(d.querySelectorAll('[role="button"], button')).find((b) => + /^(OK|Close|Got it|Dismiss)$/i.test((b.innerText || "").trim()) + ) || d.querySelector('[aria-label="Close"]'); + if (ok) { + robustClick(ok); + dismissed = true; + } + } + } + // Toast / non-dialog banners + if (/Try again later|Something went wrong/i.test(pageText())) { + if (clickText("OK")) dismissed = true; + const closes = Array.from( + document.querySelectorAll('[aria-label="Close"]') + ); + closes.slice(0, 3).forEach((c) => robustClick(c)); + if (closes.length) dismissed = true; + } + return dismissed; + } + + async function waitWhileProcessing(maxMs = 120000) { + const start = Date.now(); + while (isProcessing() && !stopFlag && Date.now() - start < maxMs) { + log("Waiting for Facebook to finish processing…"); + await sleep(3000); + } + } + + async function backoffOnError(reason) { + state.failStreak++; + state.errors++; + dismissErrorDialogs(); + const pauseSec = Math.min(300, 60 * state.failStreak); // 1m, 2m, 3m… max 5m + log(`${reason} — cooling down ${pauseSec}s (streak ${state.failStreak})`); + // Slow future deletes too + state.delayMs = Math.min(15000, state.delayMs + 1000); + const delayInput = document.getElementById("fb-cleaner-delay"); + if (delayInput) delayInput.value = String(state.delayMs); + const end = Date.now() + pauseSec * 1000; + while (!stopFlag && Date.now() < end) { + await sleep(1000); + } + dismissErrorDialogs(); + } + + async function confirmDialogIfAny() { + await sleep(500); + const dialog = document.querySelector('[role="dialog"]'); + if (!dialog) return true; + const txt = dialog.innerText || ""; + if (/Something went wrong|Please try again|Try again later/i.test(txt)) { + return false; + } + const btns = Array.from(dialog.querySelectorAll('[role="button"], button')); + const conf = btns.find((b) => + /^(Move to Trash|Trash|Delete|Remove|Confirm)$/i.test( + (b.innerText || "").trim() + ) + ); + if (conf) { + robustClick(conf); + await sleep(900); + return !hasFbError(); + } + return true; + } + + async function openMenuAndAct(btn, actionLabels) { + btn.scrollIntoView({ block: "center", behavior: "instant" }); + await sleep(300); + robustClick(btn); + await sleep(550); + + let item = null; + for (let attempt = 0; attempt < 8 && !item; attempt++) { + const items = Array.from(document.querySelectorAll('[role="menuitem"]')); + item = items.find((m) => { + const t = (m.innerText || m.getAttribute("aria-label") || "") + .trim() + .toLowerCase(); + return actionLabels.some((a) => t.includes(a.toLowerCase())); + }); + if (!item) await sleep(180); + } + + if (!item) { + document.body.click(); + await sleep(200); + return false; + } + robustClick(item); + const confirmed = await confirmDialogIfAny(); + await sleep(1000); + if (!confirmed || hasFbError()) return false; + return true; + } + + function log(line) { + state.lastAction = line; + const el = document.getElementById("fb-cleaner-log"); + if (!el) return; + const ts = new Date().toLocaleTimeString(); + el.textContent = `[${ts}] ${line}\n` + el.textContent.slice(0, 1800); + updateStats(); + } + + function updateStats() { + const el = document.getElementById("fb-cleaner-stats"); + if (!el) return; + el.innerHTML = + `
Status: ${state.running ? "Running" : "Stopped"} · Mode: ${state.mode}
` + + `
Deleted: ${state.deleted} · Skipped: ${state.skipped}
` + + `
Offensive forced: ${state.offensiveForced} · Errors: ${state.errors}
` + + `
Last: ${escapeHtml(state.lastAction)}
`; + } + + function escapeHtml(s) { + return String(s) + .replace(/&/g, "&") + .replace(//g, ">"); + } + + function classify(btn, skipTerms, offensiveTerms) { + const label = btn.getAttribute("aria-label") || ""; + const text = rowText(btn); + // Community skip must use the button label only — row blocks are huge and + // include neighboring Tesla/Ford/Pokemon items, which false-skipped everything. + const skipCommunity = matchesAny(label, skipTerms); + const offensive = matchesAny(text, offensiveTerms); + const keepPost = KEEP_POST_RE.test(label); + + if (state.mode === "posts" && keepPost) { + return { action: "skip", reason: "keep photo/profile", offensive: false }; + } + + // Always delete offensive comments, even on Tesla/Ford/Pokemon pages + if (offensive && state.mode === "comments") { + return { action: "delete", reason: "offensive", offensive: true }; + } + + if (skipCommunity) { + return { action: "skip", reason: "skip community", offensive: false }; + } + + return { action: "delete", reason: "cleanup", offensive: false }; + } + + async function waitForRowGone(btn, labelBefore, maxMs = 5000) { + const start = Date.now(); + while (!stopFlag && Date.now() - start < maxMs) { + if (!btn.isConnected) return true; + const current = btn.getAttribute("aria-label") || ""; + // React reused the node for a different row + if (current && current !== labelBefore) return true; + // Same label still present somewhere is OK if this node is gone; + // if this node still has the label, keep waiting briefly + await sleep(200); + } + return !btn.isConnected || (btn.getAttribute("aria-label") || "") !== labelBefore; + } + + async function processOne(skipTerms, offensiveTerms) { + if (hasFbError()) { + await backoffOnError("Facebook error"); + return "rate-limit"; + } + await waitWhileProcessing(); + if (stopFlag) return "stopped"; + + const btns = moreButtons(); + if (!btns.length) return "empty"; + + let chose = null; + let classification = null; + let skippedNow = 0; + for (const btn of btns) { + const c = classify(btn, skipTerms, offensiveTerms); + if (c.action === "skip") { + const lbl = btn.getAttribute("aria-label") || ""; + if (lbl && !countedSkipLabels.has(lbl)) { + countedSkipLabels.add(lbl); + state.skipped++; + skippedNow++; + } else if (!lbl) { + skippedNow++; + } + continue; + } + chose = btn; + classification = c; + break; + } + + if (!chose) { + if (skippedNow) { + log(`Skipped ${skippedNow} visible (Tesla/Ford/Pokémon/keep) — scrolling`); + } + window.scrollBy(0, 2200); + await sleep(1100); + const after = moreButtons(); + if (after.length === 0) return "exhausted-visible"; + return "scrolled"; + } + + if (skippedNow) { + log(`Passing ${skippedNow} skip-community item(s)`); + } + + const labelBefore = chose.getAttribute("aria-label") || ""; + const label = labelBefore.slice(0, 90); + log(`Deleting: ${label}`); + const ok = await openMenuAndAct( + chose, + state.mode === "posts" + ? ["move to trash", "delete", "trash"] + : ["delete", "remove"] + ); + + if (!ok || hasFbError()) { + await backoffOnError(hasFbError() ? "Something went wrong" : "Delete failed"); + return "rate-limit"; + } + + await waitForRowGone(chose, labelBefore); + + if (hasFbError()) { + await backoffOnError("Something went wrong"); + return "rate-limit"; + } + + state.deleted++; + state.failStreak = 0; + if (classification.offensive) state.offensiveForced++; + log( + `${classification.offensive ? "Offensive " : ""}Deleted: ${label}` + ); + await sleep(state.delayMs); + return "deleted"; + } + + async function runLoop() { + const skipTerms = parseList( + document.getElementById("fb-cleaner-skip")?.value || DEFAULT_SKIP + ); + const offensiveTerms = parseList( + document.getElementById("fb-cleaner-offensive")?.value || DEFAULT_OFFENSIVE + ); + const delayRaw = Number( + document.getElementById("fb-cleaner-delay")?.value || 2500 + ); + // Migrate old second-based values (< 100) to ms + state.delayMs = + delayRaw > 0 && delayRaw < 100 + ? Math.round(delayRaw * 1000) + : Math.max(0, Math.round(delayRaw || 2500)); + const delayInput = document.getElementById("fb-cleaner-delay"); + if (delayInput) delayInput.value = String(state.delayMs); + state.failStreak = 0; + countedSkipLabels.clear(); + + state.mode = detectMode(); + state.running = true; + stopFlag = false; + updateStats(); + setButtons(true); + log(`Started in ${state.mode} mode · ${state.delayMs}ms between deletes`); + + let emptyStreak = 0; + while (!stopFlag) { + if (!isActivityLog()) { + log("Not on Activity Log — open Comments or Posts manage mode"); + break; + } + state.mode = detectMode(); + const result = await processOne(skipTerms, offensiveTerms); + if (stopFlag) break; + + if (result === "empty" || result === "exhausted-visible") { + emptyStreak++; + window.scrollBy(0, 2800); + await sleep(1400); + if (moreButtons().length === 0) { + if (emptyStreak === 2) { + window.scrollTo(0, 0); + await sleep(1000); + } + } + if (emptyStreak >= 6) { + log("No more deletable items on this page"); + break; + } + } else if (result !== "rate-limit") { + emptyStreak = 0; + } + } + + state.running = false; + setButtons(false); + log("Stopped"); + updateStats(); + } + + function setButtons(running) { + const start = document.getElementById("fb-cleaner-start"); + const stop = document.getElementById("fb-cleaner-stop"); + if (start) start.disabled = running; + if (stop) stop.disabled = !running; + } + + function ensurePanel() { + if (document.getElementById("fb-cleaner-root")) return; + + const root = document.createElement("div"); + root.id = "fb-cleaner-root"; + root.innerHTML = ` +
+
+ FB Activity Cleaner + +
+
+ +
+
Skip communities (unless offensive)
+ +
+
+
Offensive phrases — always delete
+ +
+ +
+
+
+ + +
+
+ Keeps profile/cover/photo uploads on Posts. Skips Tesla/Ford/Pokémon unless offensive. Slows down automatically if Facebook rate-limits. +
+
+
+ `; + document.documentElement.appendChild(root); + + document.getElementById("fb-cleaner-start").onclick = () => { + if (state.running) return; + runLoop(); + }; + document.getElementById("fb-cleaner-stop").onclick = () => { + stopFlag = true; + log("Stop requested…"); + }; + document.getElementById("fb-cleaner-minimize").onclick = () => { + root.classList.toggle("collapsed"); + }; + + // drag + const header = document.getElementById("fb-cleaner-header"); + let dragging = false; + let ox = 0; + let oy = 0; + header.addEventListener("mousedown", (e) => { + dragging = true; + const rect = root.getBoundingClientRect(); + ox = e.clientX - rect.left; + oy = e.clientY - rect.top; + e.preventDefault(); + }); + window.addEventListener("mousemove", (e) => { + if (!dragging) return; + root.style.left = Math.max(0, e.clientX - ox) + "px"; + root.style.top = Math.max(0, e.clientY - oy) + "px"; + root.style.right = "auto"; + root.style.bottom = "auto"; + }); + window.addEventListener("mouseup", () => { + dragging = false; + }); + + chrome.storage?.local?.get(["skip", "offensive", "delay"], (data) => { + if (data?.skip) { + document.getElementById("fb-cleaner-skip").value = data.skip; + } + if (data?.offensive) { + document.getElementById("fb-cleaner-offensive").value = data.offensive; + } + if (data?.delay != null && data.delay !== "") { + const n = Number(data.delay); + const ms = n > 0 && n < 100 ? Math.round(n * 1000) : Math.round(n); + document.getElementById("fb-cleaner-delay").value = String( + Number.isFinite(ms) ? ms : 2500 + ); + } + }); + + const persist = () => { + chrome.storage?.local?.set({ + skip: document.getElementById("fb-cleaner-skip").value, + offensive: document.getElementById("fb-cleaner-offensive").value, + delay: document.getElementById("fb-cleaner-delay").value, + }); + }; + document.getElementById("fb-cleaner-skip").addEventListener("change", persist); + document + .getElementById("fb-cleaner-offensive") + .addEventListener("change", persist); + document + .getElementById("fb-cleaner-delay") + .addEventListener("change", persist); + + updateStats(); + log("Panel ready — click Start deletion"); + } + + function startFromExternal() { + ensurePanel(); + if (state.running) return { ok: true, alreadyRunning: true }; + runLoop(); + return { ok: true }; + } + + function stopFromExternal() { + stopFlag = true; + log("Stop requested…"); + return { ok: true }; + } + + chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { + if (!msg || !msg.type) return false; + if (msg.type === "FB_CLEANER_PING") { + sendResponse({ ok: true }); + return false; + } + if (msg.type === "FB_CLEANER_START") { + sendResponse(startFromExternal()); + return false; + } + if (msg.type === "FB_CLEANER_STOP") { + sendResponse(stopFromExternal()); + return false; + } + if (msg.type === "FB_CLEANER_STATUS") { + sendResponse({ ok: true, state: { ...state } }); + return false; + } + return false; + }); + + function boot() { + ensurePanel(); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", boot); + } else { + boot(); + } +})(); diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..3480722 --- /dev/null +++ b/manifest.json @@ -0,0 +1,25 @@ +{ + "manifest_version": 3, + "name": "FB Activity Cleaner", + "version": "1.0.6", + "description": "Bulk-clean Facebook Activity Log: delete comments and shared posts with skip rules.", + "permissions": ["storage", "activeTab", "scripting"], + "host_permissions": ["https://www.facebook.com/*", "https://facebook.com/*"], + "action": { + "default_title": "FB Activity Cleaner", + "default_popup": "popup.html" + }, + "content_scripts": [ + { + "matches": ["https://www.facebook.com/*", "https://facebook.com/*"], + "js": ["content.js"], + "css": ["panel.css"], + "run_at": "document_idle" + } + ], + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } +} diff --git a/panel.css b/panel.css new file mode 100644 index 0000000..b3ca14d --- /dev/null +++ b/panel.css @@ -0,0 +1,148 @@ +#fb-cleaner-root { + all: initial; + position: fixed !important; + bottom: 20px !important; + right: 20px !important; + z-index: 2147483646 !important; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important; +} + +#fb-cleaner-root * { + box-sizing: border-box; + font-family: inherit; +} + +#fb-cleaner-panel { + width: 320px; + background: #111827; + color: #f9fafb; + border-radius: 12px; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35); + overflow: hidden; + border: 1px solid #374151; +} + +#fb-cleaner-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 12px; + background: #0b1220; + cursor: move; + user-select: none; +} + +#fb-cleaner-header strong { + font-size: 13px; + font-weight: 650; +} + +#fb-cleaner-body { + padding: 12px; + display: grid; + gap: 10px; +} + +#fb-cleaner-body label { + display: flex; + gap: 8px; + align-items: flex-start; + font-size: 12px; + line-height: 1.35; + color: #e5e7eb; +} + +#fb-cleaner-body input[type="checkbox"] { + margin-top: 2px; +} + +#fb-cleaner-body textarea { + width: 100%; + min-height: 52px; + resize: vertical; + border-radius: 8px; + border: 1px solid #4b5563; + background: #1f2937; + color: #f9fafb; + padding: 8px; + font-size: 11px; + line-height: 1.35; +} + +#fb-cleaner-stats { + font-size: 12px; + line-height: 1.45; + color: #d1d5db; + background: #1f2937; + border-radius: 8px; + padding: 8px 10px; + border: 1px solid #374151; +} + +#fb-cleaner-stats b { + color: #fff; +} + +#fb-cleaner-log { + max-height: 110px; + overflow: auto; + font-size: 11px; + line-height: 1.4; + color: #9ca3af; + background: #0b1220; + border-radius: 8px; + padding: 8px; + border: 1px solid #374151; + white-space: pre-wrap; +} + +#fb-cleaner-actions { + display: flex; + gap: 8px; +} + +#fb-cleaner-actions button { + flex: 1; + border: 0; + border-radius: 8px; + padding: 9px 10px; + font-size: 12px; + font-weight: 650; + cursor: pointer; +} + +#fb-cleaner-start { + background: #2563eb; + color: #fff; + font-size: 13px; + padding: 11px 10px; +} + +#fb-cleaner-start:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +#fb-cleaner-stop { + background: #374151; + color: #fff; +} + +#fb-cleaner-stop:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +#fb-cleaner-minimize { + background: transparent; + border: 0; + color: #9ca3af; + cursor: pointer; + font-size: 14px; + line-height: 1; + padding: 2px 4px; +} + +#fb-cleaner-root.collapsed #fb-cleaner-body { + display: none; +} diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..685883f --- /dev/null +++ b/popup.html @@ -0,0 +1,84 @@ + + + + + FB Activity Cleaner + + + +

FB Activity Cleaner

+

Open Activity Log manage mode, then start deletion here or on the page panel.

+ +
+ + +
+ +
Ready
+ +
    +
  1. Open Comments or Posts Activity Log
  2. +
  3. Make sure checkboxes (manage mode) are visible
  4. +
  5. Click Start deletion
  6. +
+ + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..59e6e73 --- /dev/null +++ b/popup.js @@ -0,0 +1,77 @@ +const statusEl = document.getElementById("status"); + +function setStatus(text) { + statusEl.textContent = text; +} + +async function getFacebookTab() { + const tabs = await chrome.tabs.query({ + active: true, + currentWindow: true, + }); + const tab = tabs[0]; + if (!tab?.id) throw new Error("No active tab"); + if (!/^https:\/\/(www\.)?facebook\.com\//i.test(tab.url || "")) { + throw new Error("Open a facebook.com Activity Log tab first"); + } + return tab; +} + +async function ping(tabId) { + return chrome.tabs.sendMessage(tabId, { type: "FB_CLEANER_PING" }); +} + +async function injectContent(tabId) { + if (!chrome.scripting?.executeScript) { + throw new Error( + "Reload this extension on chrome://extensions, then refresh Facebook" + ); + } + await chrome.scripting.executeScript({ + target: { tabId }, + files: ["content.js"], + }); + if (chrome.scripting.insertCSS) { + await chrome.scripting.insertCSS({ + target: { tabId }, + files: ["panel.css"], + }); + } + // Give the content script a moment to register listeners + await new Promise((r) => setTimeout(r, 150)); +} + +async function ensureContent(tabId) { + try { + await ping(tabId); + } catch { + await injectContent(tabId); + await ping(tabId); + } +} + +async function send(type) { + const tab = await getFacebookTab(); + await ensureContent(tab.id); + return chrome.tabs.sendMessage(tab.id, { type }); +} + +document.getElementById("start").addEventListener("click", async () => { + try { + setStatus("Starting…"); + const res = await send("FB_CLEANER_START"); + setStatus(res?.ok ? "Deletion started on the page" : res?.error || "Failed"); + } catch (e) { + setStatus(e.message || String(e)); + } +}); + +document.getElementById("stop").addEventListener("click", async () => { + try { + setStatus("Stopping…"); + const res = await send("FB_CLEANER_STOP"); + setStatus(res?.ok ? "Stop requested" : res?.error || "Failed"); + } catch (e) { + setStatus(e.message || String(e)); + } +});