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