From b5e8e56e8106dc5f66724e308b05cbd97db06476 Mon Sep 17 00:00:00 2001 From: MMMMMNG <113881019+MMMMMNG@users.noreply.github.com> Date: Tue, 30 Dec 2025 18:20:15 +0100 Subject: [PATCH 01/15] initial boilerplate (gemini) event is misconfigured as function --- compile.ps1 | 1 + src/background.js | 21 +++++++ src/exxperimentHack/implementation.js | 80 +++++++++++++++++++++++++++ src/exxperimentHack/schema.json | 23 ++++++++ src/injectButtons.js | 1 + src/manifest.json | 14 ++++- 6 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 src/exxperimentHack/implementation.js create mode 100644 src/exxperimentHack/schema.json create mode 100644 src/injectButtons.js diff --git a/compile.ps1 b/compile.ps1 index 55ed3fa..0d3723f 100644 --- a/compile.ps1 +++ b/compile.ps1 @@ -62,6 +62,7 @@ $files = @( "./src/popup.html", "./src/popup.js", "./src/i18n.js", + "./src/exxperimentHack", "./src/styles.css" ) diff --git a/src/background.js b/src/background.js index d00d941..dd85f3a 100644 --- a/src/background.js +++ b/src/background.js @@ -54,6 +54,27 @@ messenger.messageDisplay.onMessageDisplayed.addListener( } ); +//IEFE +(async () => { + // 1. Initialize the experiment + await messenger.threadPaneButtons.initInjections().catch(e => console_error("mail list buttons init failed!")); + + // 2. Listen for clicks on the injected buttons + messenger.threadPaneButtons.onButtonClicked.addListener(async (messageId, buttonId) => { + console.log(`Button ${buttonId} clicked for message ${messageId}`); + + // Trigger a popup using the standard windows API + // Or fetch the message details using the messenger.messages API + let message = await messenger.messages.get(parseInt(messageId)); + + messenger.windows.create({ + url: `popup.html?subject=${encodeURIComponent(message.subject)}`, + type: "popup", + width: 300, + height: 200 + }); + }); +})(); /** * Searches for unsubscribe links and information in the message headers and body. * This function scans for standard unsubscribe headers (RFC 2369) and embedded links. diff --git a/src/exxperimentHack/implementation.js b/src/exxperimentHack/implementation.js new file mode 100644 index 0000000..3177ffc --- /dev/null +++ b/src/exxperimentHack/implementation.js @@ -0,0 +1,80 @@ +var threadPaneButtons = class extends ExtensionCommon.ExtensionAPI { + getAPI(context) { + const Services = globalThis.Services || + ChromeUtils.import("resource://gre/modules/Services.jsm").Services; + return { + threadPaneButtons: { + async initInjections() { + // Monitor windows for about:3pane + const observer = { + onOpenWindow(xulWindow) { + const domWindow = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + + domWindow.addEventListener("load", () => { + if (domWindow.location.href === "about:3pane") { + injectLogic(domWindow); + } + }, { once: true }); + } + }; + + const injectLogic = (win) => { + // The Thread Pane in Supernova is an + // We use a MutationObserver to catch new rows as they are rendered + const threadPane = win.document.getElementById("threadPane"); + if (!threadPane) return; + + const rowObserver = new win.MutationObserver((mutations) => { + for (let mutation of mutations) { + for (let node of mutation.addedNodes) { + if (node.nodeName === "TR" && node.classList.contains("collapsed")) { + modifyRow(node, win); + } + } + } + }); + + rowObserver.observe(threadPane.querySelector("tbody"), { childList: true }); + }; + + const modifyRow = (row, win) => { + // Get message key/ID from the row attribute + const messageKey = row.getAttribute("data-id"); + + // Find a cell to inject into (e.g., the subject cell) + const subjectCell = row.querySelector(".subject-column"); + if (!subjectCell || row.querySelector(".my-custom-btn")) return; + + const btn = win.document.createElement("button"); + btn.className = "my-custom-btn"; + btn.textContent = "⚡"; + btn.style.marginLeft = "5px"; + + btn.onclick = (e) => { + e.stopPropagation(); // Don't select the row + // Fire the event back to background.js + context.emit("onButtonClicked", messageKey, "lightning-action"); + }; + + subjectCell.appendChild(btn); + }; + + // Initialize for existing windows + let e = Services.wm.getEnumerator("mail:3pane"); + while (e.hasMoreElements()) { + let win = e.getNext(); + // In Supernova, we might need to dig into the tab's chromeBrowser + if (win.gTabmail) { + for (let tab of win.gTabmail.tabInfo) { + if (tab.currentAbout3Pane) injectLogic(tab.currentAbout3Pane); + } + } + } + + Services.wm.addListener(observer); + } + } + }; + } +}; \ No newline at end of file diff --git a/src/exxperimentHack/schema.json b/src/exxperimentHack/schema.json new file mode 100644 index 0000000..dae0528 --- /dev/null +++ b/src/exxperimentHack/schema.json @@ -0,0 +1,23 @@ +[ + { + "namespace": "threadPaneButtons", + "functions": [ + { + "name": "initInjections", + "type": "function", + "async": true, + "parameters": [] + } + ], + "events": [ + { + "name": "onButtonClicked", + "type": "function", + "parameters": [ + { "name": "messageId", "type": "string" }, + { "name": "buttonId", "type": "string" } + ] + } + ] + } +] \ No newline at end of file diff --git a/src/injectButtons.js b/src/injectButtons.js new file mode 100644 index 0000000..3371722 --- /dev/null +++ b/src/injectButtons.js @@ -0,0 +1 @@ +console.log("inject buttons!"); \ No newline at end of file diff --git a/src/manifest.json b/src/manifest.json index d4d0d38..5019f00 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -15,6 +15,7 @@ "messagesRead", "messagesDelete", "messagesMove", + "scripting", "theme", "https://*/*" ], @@ -42,5 +43,16 @@ } ] }, - "manifest_version": 2 + "manifest_version": 2, + + "experiment_apis": { + "threadPaneButtons": { + "schema": "exxperimentHack/schema.json", + "parent": { + "scopes": ["addon_parent"], + "script": "exxperimentHack/implementation.js", + "paths": [["threadPaneButtons"]] + } + } + } } From b32f0fe0b7217dc6a0650c0d087ca01a180eb382 Mon Sep 17 00:00:00 2001 From: MMMMMNG <113881019+MMMMMNG@users.noreply.github.com> Date: Wed, 31 Dec 2025 20:06:48 +0100 Subject: [PATCH 02/15] redo schema using https://darktrojan.github.io/generator/generator.html --- src/exxperimentHack/schema.json | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/exxperimentHack/schema.json b/src/exxperimentHack/schema.json index dae0528..a996515 100644 --- a/src/exxperimentHack/schema.json +++ b/src/exxperimentHack/schema.json @@ -4,6 +4,7 @@ "functions": [ { "name": "initInjections", + "description": "injects the javascript that adds buttons to the messages in the 3pane view", "type": "function", "async": true, "parameters": [] @@ -12,12 +13,21 @@ "events": [ { "name": "onButtonClicked", + "description": "called when a button on a message in the 3pane view is clicked", "type": "function", "parameters": [ - { "name": "messageId", "type": "string" }, - { "name": "buttonId", "type": "string" } + { + "name": "messageId", + "description": "the id of the message correponding to this event (and button)", + "type": "integer" + }, + { + "name": "buttonId", + "description": "the id of the html element of the button that emited this event", + "type": "string" + } ] } ] } -] \ No newline at end of file +] From 192bf5399f60a3a64dbd9e63404f03e7e816369c Mon Sep 17 00:00:00 2001 From: MMMMMNG <113881019+MMMMMNG@users.noreply.github.com> Date: Wed, 31 Dec 2025 20:06:59 +0100 Subject: [PATCH 03/15] first success --- src/exxperimentHack/implementation.js | 48 +++++++++++++++++++++------ 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/src/exxperimentHack/implementation.js b/src/exxperimentHack/implementation.js index 3177ffc..2599163 100644 --- a/src/exxperimentHack/implementation.js +++ b/src/exxperimentHack/implementation.js @@ -1,5 +1,13 @@ var threadPaneButtons = class extends ExtensionCommon.ExtensionAPI { getAPI(context) { + const eventPasser = { + callback: null, + pass(){ + if(this.callback){ + this.callback(...arguments); + } + } + }; const Services = globalThis.Services || ChromeUtils.import("resource://gre/modules/Services.jsm").Services; return { @@ -22,42 +30,46 @@ var threadPaneButtons = class extends ExtensionCommon.ExtensionAPI { const injectLogic = (win) => { // The Thread Pane in Supernova is an
// We use a MutationObserver to catch new rows as they are rendered - const threadPane = win.document.getElementById("threadPane"); + const threadPane = win.document.querySelector('[is="tree-view-table-body"]'); if (!threadPane) return; const rowObserver = new win.MutationObserver((mutations) => { + for (let mutation of mutations) { for (let node of mutation.addedNodes) { - if (node.nodeName === "TR" && node.classList.contains("collapsed")) { + if (node.nodeName === "tr" && node.classList.contains("card-layout")) { modifyRow(node, win); } } } }); - rowObserver.observe(threadPane.querySelector("tbody"), { childList: true }); + rowObserver.observe(threadPane, { childList: true }); }; const modifyRow = (row, win) => { // Get message key/ID from the row attribute - const messageKey = row.getAttribute("data-id"); + const rowId = row.getAttribute("id"); // Find a cell to inject into (e.g., the subject cell) - const subjectCell = row.querySelector(".subject-column"); + const subjectCell = row.querySelector(".thread-card-subject-container"); if (!subjectCell || row.querySelector(".my-custom-btn")) return; const btn = win.document.createElement("button"); btn.className = "my-custom-btn"; btn.textContent = "⚡"; + let btnId = `${rowId}-btn`; + btn.setAttribute("id", btnId); btn.style.marginLeft = "5px"; btn.onclick = (e) => { e.stopPropagation(); // Don't select the row // Fire the event back to background.js - context.emit("onButtonClicked", messageKey, "lightning-action"); + eventPasser.pass(parseInt(rowId.match(/\d$/)[0]), btnId); }; subjectCell.appendChild(btn); + subjectCell.style.display = "flex"; }; // Initialize for existing windows @@ -65,15 +77,29 @@ var threadPaneButtons = class extends ExtensionCommon.ExtensionAPI { while (e.hasMoreElements()) { let win = e.getNext(); // In Supernova, we might need to dig into the tab's chromeBrowser - if (win.gTabmail) { - for (let tab of win.gTabmail.tabInfo) { - if (tab.currentAbout3Pane) injectLogic(tab.currentAbout3Pane); - } + for(let i = 0; i < win.length; i++){ + if(win[i].location.href === "about:3pane") injectLogic(win[i]); } } Services.wm.addListener(observer); - } + }, + onButtonClicked: new ExtensionCommon.EventManager({ + context, + name: "threadPaneButtons.onButtonClicked", + register(fire) { + let listener = (rowId, btnId) => { + // Fire any listeners registered with addListener. + fire.async(rowId, btnId); + }; + // Register the listener. + eventPasser.callback = listener; + return () => { + // Return a way to unregister the listener. + eventPasser.callback = null; + }; + }, + }).api() } }; } From ca8432847d533c7f0f3f4f9a7bb8f904413dc596 Mon Sep 17 00:00:00 2001 From: MMMMMNG <113881019+MMMMMNG@users.noreply.github.com> Date: Thu, 1 Jan 2026 13:37:47 +0100 Subject: [PATCH 04/15] make popup reference correct message --- src/background.js | 56 ++++++++++++++++++--------- src/exxperimentHack/implementation.js | 7 +++- src/popup.js | 9 ++++- 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/src/background.js b/src/background.js index dd85f3a..231235d 100644 --- a/src/background.js +++ b/src/background.js @@ -19,6 +19,20 @@ function console_error() { console.error('[BetterUnsubscribe][background.js]', ...arguments); } +async function cacheUnsubMethod(message){ + let value; + if (funcCache.has(message.id)) { + // Message is in cache + value = funcCache.get(message.id); + } else { + // Message not in cache, call searchUnsub(message) + value = await searchUnsub(message); + // Store the result in cache + funcCache.set(message.id, value); + } + return value; +} + /** * Event listener for message display events. * Disables the action button initially and checks if unsubscribe information is available. @@ -32,19 +46,7 @@ messenger.messageDisplay.onMessageDisplayed.addListener( console_log('Message displayed'); await messenger.messageDisplayAction.disable(tab.id); // Disable action button until processing is complete if (message) { - let value; - - if (funcCache.has(message.id)) { - // Message is in cache - value = funcCache.get(message.id); - } else { - // Message not in cache, call searchUnsub(message) - value = await searchUnsub(message); - // Store the result in cache - funcCache.set(message.id, value); - } - - if (value !== null) { + if (await cacheUnsubMethod(message) !== null) { await messenger.messageDisplayAction.enable(tab.id); // Enable action button if unsubscribe info is found } } @@ -64,14 +66,13 @@ messenger.messageDisplay.onMessageDisplayed.addListener( console.log(`Button ${buttonId} clicked for message ${messageId}`); // Trigger a popup using the standard windows API - // Or fetch the message details using the messenger.messages API - let message = await messenger.messages.get(parseInt(messageId)); - + let message = await getNthMessage(messenger.mailTabs.getListedMessages(), messageId); + await cacheUnsubMethod(message); messenger.windows.create({ - url: `popup.html?subject=${encodeURIComponent(message.subject)}`, + url: `popup.html?messageId=${message.id}`, type: "popup", - width: 300, - height: 200 + width: 500, + height: 300 }); }); })(); @@ -539,6 +540,23 @@ async function* getMessages(list) { } } +/** + * Retrieves the nth message (0-indexed) from the getMessages async generator. + * @param {Promise} list - The initial paginated list promise. + * @param {number} n - The index of the message to retrieve. + * @returns {Promise} - The nth message or null if not found. + */ +async function getNthMessage(list, n) { + let count = 0; + for await (const message of getMessages(list)) { + if (count === n) { + return message; + } + count++; + } + return null; +} + /** * Generator function to yield messages from all inbox folders across all accounts. * This function finds and processes messages from each inbox folder. diff --git a/src/exxperimentHack/implementation.js b/src/exxperimentHack/implementation.js index 2599163..e7cb0f0 100644 --- a/src/exxperimentHack/implementation.js +++ b/src/exxperimentHack/implementation.js @@ -33,6 +33,11 @@ var threadPaneButtons = class extends ExtensionCommon.ExtensionAPI { const threadPane = win.document.querySelector('[is="tree-view-table-body"]'); if (!threadPane) return; + //add button to every currently visible mail card + for(let c of threadPane.querySelectorAll(".card-layout")){ + modifyRow(c, win); + } + const rowObserver = new win.MutationObserver((mutations) => { for (let mutation of mutations) { @@ -65,7 +70,7 @@ var threadPaneButtons = class extends ExtensionCommon.ExtensionAPI { btn.onclick = (e) => { e.stopPropagation(); // Don't select the row // Fire the event back to background.js - eventPasser.pass(parseInt(rowId.match(/\d$/)[0]), btnId); + eventPasser.pass(parseInt(rowId.match(/\d+$/)[0]), btnId); }; subjectCell.appendChild(btn); diff --git a/src/popup.js b/src/popup.js index 3fae486..3635156 100644 --- a/src/popup.js +++ b/src/popup.js @@ -27,7 +27,14 @@ document.addEventListener('DOMContentLoaded', async () => { active: true, currentWindow: true, }); - const message = await messenger.messageDisplay.getDisplayedMessage(tab.id); + + let message = await messenger.messageDisplay.getDisplayedMessage(tab.id); + //if the id is passed via url, use it + const urlParams = new URLSearchParams(document.location.search); + const mId = urlParams.get('messageId'); + if(mId){ + message = await messenger.messages.get(parseInt(mId)); + } console_log('Message', message.id); // Retrieve and cache references to various DOM elements for later use. From e0b83925e7be0317e69ebffed5e514da927a9f01 Mon Sep 17 00:00:00 2001 From: MMMMMNG <113881019+MMMMMNG@users.noreply.github.com> Date: Thu, 1 Jan 2026 13:53:49 +0100 Subject: [PATCH 05/15] refactor to seperate boilerplate --- src/exxperimentHack/implementation.js | 39 ++++++++++++++++----------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/exxperimentHack/implementation.js b/src/exxperimentHack/implementation.js index e7cb0f0..bb081ba 100644 --- a/src/exxperimentHack/implementation.js +++ b/src/exxperimentHack/implementation.js @@ -1,19 +1,6 @@ -var threadPaneButtons = class extends ExtensionCommon.ExtensionAPI { - getAPI(context) { - const eventPasser = { - callback: null, - pass(){ - if(this.callback){ - this.callback(...arguments); - } - } - }; - const Services = globalThis.Services || - ChromeUtils.import("resource://gre/modules/Services.jsm").Services; - return { - threadPaneButtons: { - async initInjections() { - // Monitor windows for about:3pane +//core logic +async function initInjectionsImpl(Services, eventPasser){ + // Monitor windows for about:3pane const observer = { onOpenWindow(xulWindow) { const domWindow = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor) @@ -88,6 +75,26 @@ var threadPaneButtons = class extends ExtensionCommon.ExtensionAPI { } Services.wm.addListener(observer); +} + + +//generated boilerplate (https://darktrojan.github.io/generator/generator.html) +var threadPaneButtons = class extends ExtensionCommon.ExtensionAPI { + getAPI(context) { + const eventPasser = { + callback: null, + pass(){ + if(this.callback){ + this.callback(...arguments); + } + } + }; + const Services = globalThis.Services || + ChromeUtils.import("resource://gre/modules/Services.jsm").Services; + return { + threadPaneButtons: { + async initInjections() { + await initInjectionsImpl(Services, eventPasser); }, onButtonClicked: new ExtensionCommon.EventManager({ context, From 2c41b5f52d7960f2841e41e2f3ee09e6792e890a Mon Sep 17 00:00:00 2001 From: MMMMMNG <113881019+MMMMMNG@users.noreply.github.com> Date: Thu, 1 Jan 2026 14:43:07 +0100 Subject: [PATCH 06/15] enable buttons only on mails with unsub method --- src/background.js | 29 +++++++++++-- src/exxperimentHack/implementation.js | 60 +++++++++++++++++++++++---- src/exxperimentHack/schema.json | 25 +++++++++++ 3 files changed, 101 insertions(+), 13 deletions(-) diff --git a/src/background.js b/src/background.js index 231235d..57e8a99 100644 --- a/src/background.js +++ b/src/background.js @@ -20,7 +20,7 @@ function console_error() { } async function cacheUnsubMethod(message){ - let value; + let value = null; if (funcCache.has(message.id)) { // Message is in cache value = funcCache.get(message.id); @@ -56,12 +56,17 @@ messenger.messageDisplay.onMessageDisplayed.addListener( } ); -//IEFE +/** + * IEFE for injecting the js into the dom that adds buttons (hacky af) + */ (async () => { // 1. Initialize the experiment await messenger.threadPaneButtons.initInjections().catch(e => console_error("mail list buttons init failed!")); +})(); - // 2. Listen for clicks on the injected buttons +/** + * listen for a button being clicked in the table view and search for the mail and open a popup + */ messenger.threadPaneButtons.onButtonClicked.addListener(async (messageId, buttonId) => { console.log(`Button ${buttonId} clicked for message ${messageId}`); @@ -75,7 +80,23 @@ messenger.messageDisplay.onMessageDisplayed.addListener( height: 300 }); }); -})(); + +/** + * listen for a button being added to the table view. these buttons are disabled by default. + * if the corr. email has an unsub method, enable the button + */ + messenger.threadPaneButtons.onButtonProduced.addListener(async (rowNo) => { + console.log(`Button in row ${rowNo} added to dom`); + + // find message + const message = await getNthMessage(messenger.mailTabs.getListedMessages(), rowNo); + //see if message has unsub method + if(await cacheUnsubMethod(message)){ + //if yes, enable button + await messenger.threadPaneButtons.enableButton(rowNo); + } + }); + /** * Searches for unsubscribe links and information in the message headers and body. * This function scans for standard unsubscribe headers (RFC 2369) and embedded links. diff --git a/src/exxperimentHack/implementation.js b/src/exxperimentHack/implementation.js index bb081ba..1dc4a97 100644 --- a/src/exxperimentHack/implementation.js +++ b/src/exxperimentHack/implementation.js @@ -1,4 +1,15 @@ //core logic +function get3panewindow(services){ + let e = services.wm.getEnumerator("mail:3pane"); + while (e.hasMoreElements()) { + let win = e.getNext(); + // In Supernova, we might need to dig into the tab's chromeBrowser + for(let i = 0; i < win.length; i++){ + if(win[i].location.href === "about:3pane") return win[i]; + } + } +} + async function initInjectionsImpl(Services, eventPasser){ // Monitor windows for about:3pane const observer = { @@ -42,6 +53,7 @@ async function initInjectionsImpl(Services, eventPasser){ const modifyRow = (row, win) => { // Get message key/ID from the row attribute const rowId = row.getAttribute("id"); + const rowNo = parseInt(rowId.match(/\d+$/)[0]); // Find a cell to inject into (e.g., the subject cell) const subjectCell = row.querySelector(".thread-card-subject-container"); @@ -53,40 +65,51 @@ async function initInjectionsImpl(Services, eventPasser){ let btnId = `${rowId}-btn`; btn.setAttribute("id", btnId); btn.style.marginLeft = "5px"; + btn.style.display = "none"; btn.onclick = (e) => { e.stopPropagation(); // Don't select the row // Fire the event back to background.js - eventPasser.pass(parseInt(rowId.match(/\d+$/)[0]), btnId); + eventPasser.pass(rowNo, btnId); //notify background.js about button click }; subjectCell.appendChild(btn); + eventPasser.buttonPass(rowNo); //notify background.js about adding of new button subjectCell.style.display = "flex"; }; // Initialize for existing windows - let e = Services.wm.getEnumerator("mail:3pane"); - while (e.hasMoreElements()) { - let win = e.getNext(); - // In Supernova, we might need to dig into the tab's chromeBrowser - for(let i = 0; i < win.length; i++){ - if(win[i].location.href === "about:3pane") injectLogic(win[i]); - } - } + injectLogic(get3panewindow(Services)); Services.wm.addListener(observer); } +async function enableButtonImpl(services, rowNo){ + let win = get3panewindow(services); + let el = win.document.querySelector(`#threadTree-row${rowNo}-btn`); + if(el){ + el.style.display = "block"; + } +} + //generated boilerplate (https://darktrojan.github.io/generator/generator.html) var threadPaneButtons = class extends ExtensionCommon.ExtensionAPI { getAPI(context) { + //hacky object for passing events from the dom to the background. Am pleasantly + //surprised that this works but there's probably a better way to do this tbh. const eventPasser = { callback: null, + buttonCallback: null, pass(){ if(this.callback){ this.callback(...arguments); } + }, + buttonPass(){ + if(this.buttonCallback){ + this.buttonCallback(...arguments); + } } }; const Services = globalThis.Services || @@ -96,6 +119,9 @@ var threadPaneButtons = class extends ExtensionCommon.ExtensionAPI { async initInjections() { await initInjectionsImpl(Services, eventPasser); }, + async enableButton(rowNo) { + await enableButtonImpl(Services, rowNo); + }, onButtonClicked: new ExtensionCommon.EventManager({ context, name: "threadPaneButtons.onButtonClicked", @@ -111,6 +137,22 @@ var threadPaneButtons = class extends ExtensionCommon.ExtensionAPI { eventPasser.callback = null; }; }, + }).api(), + onButtonProduced: new ExtensionCommon.EventManager({ + context, + name: "threadPaneButtons.onButtonProduced", + register(fire) { + let listener = (rowNo) => { + // Fire any listeners registered with addListener. + fire.async(rowNo); + }; + // Register the listener. + eventPasser.buttonCallback = listener; + return () => { + // Return a way to unregister the listener. + eventPasser.buttonCallback = null; + }; + }, }).api() } }; diff --git a/src/exxperimentHack/schema.json b/src/exxperimentHack/schema.json index a996515..102a37a 100644 --- a/src/exxperimentHack/schema.json +++ b/src/exxperimentHack/schema.json @@ -8,6 +8,19 @@ "type": "function", "async": true, "parameters": [] + }, + { + "name": "enableButton", + "description": "enables the button in a specific row, should only be called if the corresponding mail has an unsub method.", + "type": "function", + "async": true, + "parameters": [ + { + "name": "rowNo", + "description": "the row for which the button should be enabled", + "type": "integer" + } + ] } ], "events": [ @@ -27,6 +40,18 @@ "type": "string" } ] + }, + { + "name": "onButtonProduced", + "description": "fired whenever a button is added to the dom. Button is disabled and must be enabled by a call to enable button (if the mail has an unsub method)", + "type": "function", + "parameters": [ + { + "name": "rowNo", + "description": "the number of the mail in the current list, meaning the number of the row to which this button was added.", + "type": "integer" + } + ] } ] } From 637f72adf98b852e3dd7405491606f242e680b73 Mon Sep 17 00:00:00 2001 From: MMMMMNG <113881019+MMMMMNG@users.noreply.github.com> Date: Thu, 1 Jan 2026 15:13:03 +0100 Subject: [PATCH 07/15] reload on close also remove unused param of the api --- src/background.js | 8 +++++--- src/exxperimentHack/implementation.js | 20 ++++++++++++++------ src/exxperimentHack/schema.json | 9 ++------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/background.js b/src/background.js index 57e8a99..b4bec63 100644 --- a/src/background.js +++ b/src/background.js @@ -66,12 +66,13 @@ messenger.messageDisplay.onMessageDisplayed.addListener( /** * listen for a button being clicked in the table view and search for the mail and open a popup + * @param {integer} rowNo - the number of the row in the table view whose button was clicked */ - messenger.threadPaneButtons.onButtonClicked.addListener(async (messageId, buttonId) => { - console.log(`Button ${buttonId} clicked for message ${messageId}`); + messenger.threadPaneButtons.onButtonClicked.addListener(async (rowNo) => { + console.log(`Button in row ${rowNo} clicked for message`); // Trigger a popup using the standard windows API - let message = await getNthMessage(messenger.mailTabs.getListedMessages(), messageId); + let message = await getNthMessage(messenger.mailTabs.getListedMessages(), rowNo); await cacheUnsubMethod(message); messenger.windows.create({ url: `popup.html?messageId=${message.id}`, @@ -84,6 +85,7 @@ messenger.messageDisplay.onMessageDisplayed.addListener( /** * listen for a button being added to the table view. these buttons are disabled by default. * if the corr. email has an unsub method, enable the button + * @param {integer} rowNo - the number of the row in the table view whose button was added to the dom */ messenger.threadPaneButtons.onButtonProduced.addListener(async (rowNo) => { console.log(`Button in row ${rowNo} added to dom`); diff --git a/src/exxperimentHack/implementation.js b/src/exxperimentHack/implementation.js index 1dc4a97..d2bbafe 100644 --- a/src/exxperimentHack/implementation.js +++ b/src/exxperimentHack/implementation.js @@ -10,7 +10,7 @@ function get3panewindow(services){ } } -async function initInjectionsImpl(Services, eventPasser){ +async function initInjectionsImpl(Services, eventPasser, context){ // Monitor windows for about:3pane const observer = { onOpenWindow(xulWindow) { @@ -70,7 +70,7 @@ async function initInjectionsImpl(Services, eventPasser){ btn.onclick = (e) => { e.stopPropagation(); // Don't select the row // Fire the event back to background.js - eventPasser.pass(rowNo, btnId); //notify background.js about button click + eventPasser.pass(rowNo); //notify background.js about button click }; subjectCell.appendChild(btn); @@ -79,9 +79,17 @@ async function initInjectionsImpl(Services, eventPasser){ }; // Initialize for existing windows - injectLogic(get3panewindow(Services)); + const panewin = get3panewindow(Services); + injectLogic(panewin); Services.wm.addListener(observer); + + //clean up all the stateful injections by reloading the window + context.callOnClose({ + close() { + panewin.location.reload(); + } + }); } async function enableButtonImpl(services, rowNo){ @@ -117,7 +125,7 @@ var threadPaneButtons = class extends ExtensionCommon.ExtensionAPI { return { threadPaneButtons: { async initInjections() { - await initInjectionsImpl(Services, eventPasser); + await initInjectionsImpl(Services, eventPasser, context); }, async enableButton(rowNo) { await enableButtonImpl(Services, rowNo); @@ -126,9 +134,9 @@ var threadPaneButtons = class extends ExtensionCommon.ExtensionAPI { context, name: "threadPaneButtons.onButtonClicked", register(fire) { - let listener = (rowId, btnId) => { + let listener = (rowNo) => { // Fire any listeners registered with addListener. - fire.async(rowId, btnId); + fire.async(rowNo); }; // Register the listener. eventPasser.callback = listener; diff --git a/src/exxperimentHack/schema.json b/src/exxperimentHack/schema.json index 102a37a..d289aa1 100644 --- a/src/exxperimentHack/schema.json +++ b/src/exxperimentHack/schema.json @@ -30,14 +30,9 @@ "type": "function", "parameters": [ { - "name": "messageId", - "description": "the id of the message correponding to this event (and button)", + "name": "rowNo", + "description": "the row number of this button and message in the table view.", "type": "integer" - }, - { - "name": "buttonId", - "description": "the id of the html element of the button that emited this event", - "type": "string" } ] }, From 93d2d6c95c2425c382017e2787198da6e6eec913 Mon Sep 17 00:00:00 2001 From: MMMMMNG <113881019+MMMMMNG@users.noreply.github.com> Date: Thu, 1 Jan 2026 15:14:54 +0100 Subject: [PATCH 08/15] use canon logging --- src/background.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/background.js b/src/background.js index b4bec63..1aff2bd 100644 --- a/src/background.js +++ b/src/background.js @@ -69,7 +69,7 @@ messenger.messageDisplay.onMessageDisplayed.addListener( * @param {integer} rowNo - the number of the row in the table view whose button was clicked */ messenger.threadPaneButtons.onButtonClicked.addListener(async (rowNo) => { - console.log(`Button in row ${rowNo} clicked for message`); + console_log(`Button in row ${rowNo} clicked`); // Trigger a popup using the standard windows API let message = await getNthMessage(messenger.mailTabs.getListedMessages(), rowNo); @@ -88,8 +88,6 @@ messenger.messageDisplay.onMessageDisplayed.addListener( * @param {integer} rowNo - the number of the row in the table view whose button was added to the dom */ messenger.threadPaneButtons.onButtonProduced.addListener(async (rowNo) => { - console.log(`Button in row ${rowNo} added to dom`); - // find message const message = await getNthMessage(messenger.mailTabs.getListedMessages(), rowNo); //see if message has unsub method From ebf78e65e7808c211e46d5bfa1617f93f2fa22e9 Mon Sep 17 00:00:00 2001 From: MMMMMNG <113881019+MMMMMNG@users.noreply.github.com> Date: Thu, 1 Jan 2026 15:19:11 +0100 Subject: [PATCH 09/15] try avoiding scroll lag with non-awaited async IEFE no luck --- src/background.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/background.js b/src/background.js index 1aff2bd..8882dad 100644 --- a/src/background.js +++ b/src/background.js @@ -88,13 +88,15 @@ messenger.messageDisplay.onMessageDisplayed.addListener( * @param {integer} rowNo - the number of the row in the table view whose button was added to the dom */ messenger.threadPaneButtons.onButtonProduced.addListener(async (rowNo) => { - // find message - const message = await getNthMessage(messenger.mailTabs.getListedMessages(), rowNo); - //see if message has unsub method - if(await cacheUnsubMethod(message)){ - //if yes, enable button - await messenger.threadPaneButtons.enableButton(rowNo); - } + (async function(){ + // find message + const message = await getNthMessage(messenger.mailTabs.getListedMessages(), rowNo); + //see if message has unsub method + if(await cacheUnsubMethod(message)){ + //if yes, enable button + await messenger.threadPaneButtons.enableButton(rowNo); + } + })(); }); /** From 14cc336fb6c31a91ceac518f7a32995e2ceeba41 Mon Sep 17 00:00:00 2001 From: MMMMMNG <113881019+MMMMMNG@users.noreply.github.com> Date: Thu, 1 Jan 2026 15:29:53 +0100 Subject: [PATCH 10/15] try avoiding scroll lag by async pass method no luck --- src/exxperimentHack/implementation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exxperimentHack/implementation.js b/src/exxperimentHack/implementation.js index d2bbafe..2e29ac2 100644 --- a/src/exxperimentHack/implementation.js +++ b/src/exxperimentHack/implementation.js @@ -114,7 +114,7 @@ var threadPaneButtons = class extends ExtensionCommon.ExtensionAPI { this.callback(...arguments); } }, - buttonPass(){ + async buttonPass(){ if(this.buttonCallback){ this.buttonCallback(...arguments); } From 26a636bcb369782052c0a130797ec511c8d43636 Mon Sep 17 00:00:00 2001 From: MMMMMNG <113881019+MMMMMNG@users.noreply.github.com> Date: Thu, 1 Jan 2026 15:35:01 +0100 Subject: [PATCH 11/15] switch order of adding listener and init-ing --- src/background.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/background.js b/src/background.js index 8882dad..4e8babc 100644 --- a/src/background.js +++ b/src/background.js @@ -56,13 +56,6 @@ messenger.messageDisplay.onMessageDisplayed.addListener( } ); -/** - * IEFE for injecting the js into the dom that adds buttons (hacky af) - */ -(async () => { - // 1. Initialize the experiment - await messenger.threadPaneButtons.initInjections().catch(e => console_error("mail list buttons init failed!")); -})(); /** * listen for a button being clicked in the table view and search for the mail and open a popup @@ -85,6 +78,7 @@ messenger.messageDisplay.onMessageDisplayed.addListener( /** * listen for a button being added to the table view. these buttons are disabled by default. * if the corr. email has an unsub method, enable the button + * do this before calling initInjections so currently visible mails are also handeled * @param {integer} rowNo - the number of the row in the table view whose button was added to the dom */ messenger.threadPaneButtons.onButtonProduced.addListener(async (rowNo) => { @@ -99,6 +93,15 @@ messenger.messageDisplay.onMessageDisplayed.addListener( })(); }); + /** + * IEFE for injecting the js into the dom that adds buttons (hacky af) + * init after adding listener to onButtonProduced so currently visible mails are also handeled + */ +(async () => { + // 1. Initialize the experiment + await messenger.threadPaneButtons.initInjections().catch(e => console_error("mail list buttons init failed!")); +})(); + /** * Searches for unsubscribe links and information in the message headers and body. * This function scans for standard unsubscribe headers (RFC 2369) and embedded links. From fca66c4d7fd80d1be8cbff6ab2ef0500570217ec Mon Sep 17 00:00:00 2001 From: MMMMMNG <113881019+MMMMMNG@users.noreply.github.com> Date: Thu, 1 Jan 2026 15:41:58 +0100 Subject: [PATCH 12/15] format files --- src/background.js | 81 +++++---- src/exxperimentHack/implementation.js | 249 +++++++++++++------------- src/popup.js | 2 +- 3 files changed, 174 insertions(+), 158 deletions(-) diff --git a/src/background.js b/src/background.js index 4e8babc..91c57e0 100644 --- a/src/background.js +++ b/src/background.js @@ -19,18 +19,18 @@ function console_error() { console.error('[BetterUnsubscribe][background.js]', ...arguments); } -async function cacheUnsubMethod(message){ +async function cacheUnsubMethod(message) { let value = null; - if (funcCache.has(message.id)) { - // Message is in cache - value = funcCache.get(message.id); - } else { - // Message not in cache, call searchUnsub(message) - value = await searchUnsub(message); - // Store the result in cache - funcCache.set(message.id, value); - } - return value; + if (funcCache.has(message.id)) { + // Message is in cache + value = funcCache.get(message.id); + } else { + // Message not in cache, call searchUnsub(message) + value = await searchUnsub(message); + // Store the result in cache + funcCache.set(message.id, value); + } + return value; } /** @@ -46,7 +46,7 @@ messenger.messageDisplay.onMessageDisplayed.addListener( console_log('Message displayed'); await messenger.messageDisplayAction.disable(tab.id); // Disable action button until processing is complete if (message) { - if (await cacheUnsubMethod(message) !== null) { + if ((await cacheUnsubMethod(message)) !== null) { await messenger.messageDisplayAction.enable(tab.id); // Enable action button if unsubscribe info is found } } @@ -56,24 +56,26 @@ messenger.messageDisplay.onMessageDisplayed.addListener( } ); - /** * listen for a button being clicked in the table view and search for the mail and open a popup * @param {integer} rowNo - the number of the row in the table view whose button was clicked */ - messenger.threadPaneButtons.onButtonClicked.addListener(async (rowNo) => { - console_log(`Button in row ${rowNo} clicked`); - - // Trigger a popup using the standard windows API - let message = await getNthMessage(messenger.mailTabs.getListedMessages(), rowNo); - await cacheUnsubMethod(message); - messenger.windows.create({ - url: `popup.html?messageId=${message.id}`, - type: "popup", - width: 500, - height: 300 - }); +messenger.threadPaneButtons.onButtonClicked.addListener(async (rowNo) => { + console_log(`Button in row ${rowNo} clicked`); + + // Trigger a popup using the standard windows API + let message = await getNthMessage( + messenger.mailTabs.getListedMessages(), + rowNo + ); + await cacheUnsubMethod(message); + messenger.windows.create({ + url: `popup.html?messageId=${message.id}`, + type: 'popup', + width: 500, + height: 300, }); +}); /** * listen for a button being added to the table view. these buttons are disabled by default. @@ -81,25 +83,30 @@ messenger.messageDisplay.onMessageDisplayed.addListener( * do this before calling initInjections so currently visible mails are also handeled * @param {integer} rowNo - the number of the row in the table view whose button was added to the dom */ - messenger.threadPaneButtons.onButtonProduced.addListener(async (rowNo) => { - (async function(){ - // find message - const message = await getNthMessage(messenger.mailTabs.getListedMessages(), rowNo); - //see if message has unsub method - if(await cacheUnsubMethod(message)){ +messenger.threadPaneButtons.onButtonProduced.addListener(async (rowNo) => { + (async function () { + // find message + const message = await getNthMessage( + messenger.mailTabs.getListedMessages(), + rowNo + ); + //see if message has unsub method + if (await cacheUnsubMethod(message)) { //if yes, enable button - await messenger.threadPaneButtons.enableButton(rowNo); - } - })(); - }); + await messenger.threadPaneButtons.enableButton(rowNo); + } + })(); +}); - /** +/** * IEFE for injecting the js into the dom that adds buttons (hacky af) * init after adding listener to onButtonProduced so currently visible mails are also handeled */ (async () => { // 1. Initialize the experiment - await messenger.threadPaneButtons.initInjections().catch(e => console_error("mail list buttons init failed!")); + await messenger.threadPaneButtons + .initInjections() + .catch((e) => console_error('mail list buttons init failed!')); })(); /** diff --git a/src/exxperimentHack/implementation.js b/src/exxperimentHack/implementation.js index 2e29ac2..15c1b49 100644 --- a/src/exxperimentHack/implementation.js +++ b/src/exxperimentHack/implementation.js @@ -1,106 +1,114 @@ //core logic -function get3panewindow(services){ - let e = services.wm.getEnumerator("mail:3pane"); - while (e.hasMoreElements()) { - let win = e.getNext(); - // In Supernova, we might need to dig into the tab's chromeBrowser - for(let i = 0; i < win.length; i++){ - if(win[i].location.href === "about:3pane") return win[i]; - } - } +function get3panewindow(services) { + let e = services.wm.getEnumerator('mail:3pane'); + while (e.hasMoreElements()) { + let win = e.getNext(); + // In Supernova, we might need to dig into the tab's chromeBrowser + for (let i = 0; i < win.length; i++) { + if (win[i].location.href === 'about:3pane') return win[i]; + } + } } -async function initInjectionsImpl(Services, eventPasser, context){ - // Monitor windows for about:3pane - const observer = { - onOpenWindow(xulWindow) { - const domWindow = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow); - - domWindow.addEventListener("load", () => { - if (domWindow.location.href === "about:3pane") { - injectLogic(domWindow); - } - }, { once: true }); - } - }; - - const injectLogic = (win) => { - // The Thread Pane in Supernova is an
- // We use a MutationObserver to catch new rows as they are rendered - const threadPane = win.document.querySelector('[is="tree-view-table-body"]'); - if (!threadPane) return; - - //add button to every currently visible mail card - for(let c of threadPane.querySelectorAll(".card-layout")){ - modifyRow(c, win); - } - - const rowObserver = new win.MutationObserver((mutations) => { - - for (let mutation of mutations) { - for (let node of mutation.addedNodes) { - if (node.nodeName === "tr" && node.classList.contains("card-layout")) { - modifyRow(node, win); - } - } - } - }); - - rowObserver.observe(threadPane, { childList: true }); - }; - - const modifyRow = (row, win) => { - // Get message key/ID from the row attribute - const rowId = row.getAttribute("id"); - const rowNo = parseInt(rowId.match(/\d+$/)[0]); - - // Find a cell to inject into (e.g., the subject cell) - const subjectCell = row.querySelector(".thread-card-subject-container"); - if (!subjectCell || row.querySelector(".my-custom-btn")) return; - - const btn = win.document.createElement("button"); - btn.className = "my-custom-btn"; - btn.textContent = "⚡"; - let btnId = `${rowId}-btn`; - btn.setAttribute("id", btnId); - btn.style.marginLeft = "5px"; - btn.style.display = "none"; - - btn.onclick = (e) => { - e.stopPropagation(); // Don't select the row - // Fire the event back to background.js - eventPasser.pass(rowNo); //notify background.js about button click - }; +async function initInjectionsImpl(Services, eventPasser, context) { + // Monitor windows for about:3pane + const observer = { + onOpenWindow(xulWindow) { + const domWindow = xulWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + + domWindow.addEventListener( + 'load', + () => { + if (domWindow.location.href === 'about:3pane') { + injectLogic(domWindow); + } + }, + { once: true } + ); + }, + }; + + const injectLogic = (win) => { + // The Thread Pane in Supernova is an
+ // We use a MutationObserver to catch new rows as they are rendered + const threadPane = win.document.querySelector( + '[is="tree-view-table-body"]' + ); + if (!threadPane) return; + + //add button to every currently visible mail card + for (let c of threadPane.querySelectorAll('.card-layout')) { + modifyRow(c, win); + } + + const rowObserver = new win.MutationObserver((mutations) => { + for (let mutation of mutations) { + for (let node of mutation.addedNodes) { + if ( + node.nodeName === 'tr' && + node.classList.contains('card-layout') + ) { + modifyRow(node, win); + } + } + } + }); + + rowObserver.observe(threadPane, { childList: true }); + }; + + const modifyRow = (row, win) => { + // Get message key/ID from the row attribute + const rowId = row.getAttribute('id'); + const rowNo = parseInt(rowId.match(/\d+$/)[0]); + + // Find a cell to inject into (e.g., the subject cell) + const subjectCell = row.querySelector('.thread-card-subject-container'); + if (!subjectCell || row.querySelector('.my-custom-btn')) return; + + const btn = win.document.createElement('button'); + btn.className = 'my-custom-btn'; + btn.textContent = '⚡'; + let btnId = `${rowId}-btn`; + btn.setAttribute('id', btnId); + btn.style.marginLeft = '5px'; + btn.style.display = 'none'; - subjectCell.appendChild(btn); - eventPasser.buttonPass(rowNo); //notify background.js about adding of new button - subjectCell.style.display = "flex"; - }; - - // Initialize for existing windows - const panewin = get3panewindow(Services); - injectLogic(panewin); - - Services.wm.addListener(observer); - - //clean up all the stateful injections by reloading the window - context.callOnClose({ - close() { - panewin.location.reload(); - } - }); + btn.onclick = (e) => { + e.stopPropagation(); // Don't select the row + // Fire the event back to background.js + eventPasser.pass(rowNo); //notify background.js about button click + }; + + subjectCell.appendChild(btn); + eventPasser.buttonPass(rowNo); //notify background.js about adding of new button + subjectCell.style.display = 'flex'; + }; + + // Initialize for existing windows + const panewin = get3panewindow(Services); + injectLogic(panewin); + + Services.wm.addListener(observer); + + //clean up all the stateful injections by reloading the window + context.callOnClose({ + close() { + panewin.location.reload(); + }, + }); } -async function enableButtonImpl(services, rowNo){ +async function enableButtonImpl(services, rowNo) { let win = get3panewindow(services); let el = win.document.querySelector(`#threadTree-row${rowNo}-btn`); - if(el){ - el.style.display = "block"; + if (el) { + el.style.display = 'block'; } } - //generated boilerplate (https://darktrojan.github.io/generator/generator.html) var threadPaneButtons = class extends ExtensionCommon.ExtensionAPI { getAPI(context) { @@ -109,19 +117,20 @@ var threadPaneButtons = class extends ExtensionCommon.ExtensionAPI { const eventPasser = { callback: null, buttonCallback: null, - pass(){ - if(this.callback){ + pass() { + if (this.callback) { this.callback(...arguments); } }, - async buttonPass(){ - if(this.buttonCallback){ + async buttonPass() { + if (this.buttonCallback) { this.buttonCallback(...arguments); } - } + }, }; - const Services = globalThis.Services || - ChromeUtils.import("resource://gre/modules/Services.jsm").Services; + const Services = + globalThis.Services || + ChromeUtils.import('resource://gre/modules/Services.jsm').Services; return { threadPaneButtons: { async initInjections() { @@ -132,37 +141,37 @@ var threadPaneButtons = class extends ExtensionCommon.ExtensionAPI { }, onButtonClicked: new ExtensionCommon.EventManager({ context, - name: "threadPaneButtons.onButtonClicked", + name: 'threadPaneButtons.onButtonClicked', register(fire) { - let listener = (rowNo) => { + let listener = (rowNo) => { // Fire any listeners registered with addListener. fire.async(rowNo); - }; - // Register the listener. - eventPasser.callback = listener; - return () => { - // Return a way to unregister the listener. - eventPasser.callback = null; - }; + }; + // Register the listener. + eventPasser.callback = listener; + return () => { + // Return a way to unregister the listener. + eventPasser.callback = null; + }; }, }).api(), onButtonProduced: new ExtensionCommon.EventManager({ context, - name: "threadPaneButtons.onButtonProduced", + name: 'threadPaneButtons.onButtonProduced', register(fire) { - let listener = (rowNo) => { + let listener = (rowNo) => { // Fire any listeners registered with addListener. fire.async(rowNo); - }; - // Register the listener. - eventPasser.buttonCallback = listener; - return () => { - // Return a way to unregister the listener. - eventPasser.buttonCallback = null; - }; + }; + // Register the listener. + eventPasser.buttonCallback = listener; + return () => { + // Return a way to unregister the listener. + eventPasser.buttonCallback = null; + }; }, - }).api() - } + }).api(), + }, }; } -}; \ No newline at end of file +}; diff --git a/src/popup.js b/src/popup.js index 3635156..250c38e 100644 --- a/src/popup.js +++ b/src/popup.js @@ -32,7 +32,7 @@ document.addEventListener('DOMContentLoaded', async () => { //if the id is passed via url, use it const urlParams = new URLSearchParams(document.location.search); const mId = urlParams.get('messageId'); - if(mId){ + if (mId) { message = await messenger.messages.get(parseInt(mId)); } console_log('Message', message.id); From d43b0c792a7335f3d27c7a46e9a2f12e1245257b Mon Sep 17 00:00:00 2001 From: MMMMMNG <113881019+MMMMMNG@users.noreply.github.com> Date: Thu, 1 Jan 2026 16:01:19 +0100 Subject: [PATCH 13/15] fix 2 linting errors the rest is maybe not fixable (or not mine) --- src/background.js | 2 +- src/exxperimentHack/implementation.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/background.js b/src/background.js index 91c57e0..8655c18 100644 --- a/src/background.js +++ b/src/background.js @@ -106,7 +106,7 @@ messenger.threadPaneButtons.onButtonProduced.addListener(async (rowNo) => { // 1. Initialize the experiment await messenger.threadPaneButtons .initInjections() - .catch((e) => console_error('mail list buttons init failed!')); + .catch((_) => console_error('mail list buttons init failed!')); })(); /** diff --git a/src/exxperimentHack/implementation.js b/src/exxperimentHack/implementation.js index 15c1b49..4b5a3b7 100644 --- a/src/exxperimentHack/implementation.js +++ b/src/exxperimentHack/implementation.js @@ -8,10 +8,14 @@ function get3panewindow(services) { if (win[i].location.href === 'about:3pane') return win[i]; } } + return null; //couldn't find the win... TODO: handle this better (error?) } async function initInjectionsImpl(Services, eventPasser, context) { // Monitor windows for about:3pane + //TODO: + //this code for observing newly opened windows is untested. + //is it even necessary if the main email tab cannot be closed? const observer = { onOpenWindow(xulWindow) { const domWindow = xulWindow From 41bfd84abf592231708f9450d7d1e5c3d7b00058 Mon Sep 17 00:00:00 2001 From: MMMMMNG <113881019+MMMMMNG@users.noreply.github.com> Date: Thu, 1 Jan 2026 16:59:41 +0100 Subject: [PATCH 14/15] delete remnants --- src/injectButtons.js | 1 - src/manifest.json | 1 - 2 files changed, 2 deletions(-) delete mode 100644 src/injectButtons.js diff --git a/src/injectButtons.js b/src/injectButtons.js deleted file mode 100644 index 3371722..0000000 --- a/src/injectButtons.js +++ /dev/null @@ -1 +0,0 @@ -console.log("inject buttons!"); \ No newline at end of file diff --git a/src/manifest.json b/src/manifest.json index 5019f00..2af42bd 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -15,7 +15,6 @@ "messagesRead", "messagesDelete", "messagesMove", - "scripting", "theme", "https://*/*" ], From 306c0425031b4b8d8c6968f250064204a6909e1e Mon Sep 17 00:00:00 2001 From: MMMMMNG <113881019+MMMMMNG@users.noreply.github.com> Date: Thu, 1 Jan 2026 17:25:48 +0100 Subject: [PATCH 15/15] fix error when mail tab is not active this might've fixed the issue "buttons may not appear for emails that are already visible" --- src/background.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/background.js b/src/background.js index 8655c18..d53f46f 100644 --- a/src/background.js +++ b/src/background.js @@ -56,6 +56,14 @@ messenger.messageDisplay.onMessageDisplayed.addListener( } ); +/** + * cache the first mail tab for lookup later + * //TODO: can there be multiple mailtabs in multiple windows? + * // then we need a way to pass the tab id back and forth between implementation.js and background.js, + * // most likely by adjusting schea.json (the api) + */ +const thisMailTabPromise = messenger.mailTabs.query().then(a => a[0]); + /** * listen for a button being clicked in the table view and search for the mail and open a popup * @param {integer} rowNo - the number of the row in the table view whose button was clicked @@ -65,7 +73,7 @@ messenger.threadPaneButtons.onButtonClicked.addListener(async (rowNo) => { // Trigger a popup using the standard windows API let message = await getNthMessage( - messenger.mailTabs.getListedMessages(), + messenger.mailTabs.getListedMessages((await thisMailTabPromise).id), rowNo ); await cacheUnsubMethod(message); @@ -87,7 +95,7 @@ messenger.threadPaneButtons.onButtonProduced.addListener(async (rowNo) => { (async function () { // find message const message = await getNthMessage( - messenger.mailTabs.getListedMessages(), + messenger.mailTabs.getListedMessages((await thisMailTabPromise).id), rowNo ); //see if message has unsub method