From 58d5f2176c94520b65f7b0b56d27bbf307418157 Mon Sep 17 00:00:00 2001 From: Martin Pecka Date: Tue, 1 Nov 2022 03:28:30 +0100 Subject: [PATCH] Fixed addon on TB 102. --- api/NotifyTools/implementation.js | 152 +++++++++++++++------------ api/WindowListener/CHANGELOG.md | 4 + api/WindowListener/implementation.js | 84 +++++++-------- background.js | 10 +- content/moresnooze.js | 70 ++++++++---- content/notifyTools.js | 89 +++++++++++----- manifest.json | 5 +- 7 files changed, 247 insertions(+), 167 deletions(-) diff --git a/api/NotifyTools/implementation.js b/api/NotifyTools/implementation.js index 000b0db..a5b1259 100644 --- a/api/NotifyTools/implementation.js +++ b/api/NotifyTools/implementation.js @@ -2,7 +2,16 @@ * This file is provided by the addon-developer-support repository at * https://github.com/thundernest/addon-developer-support * + * Version 1.4 + * - updated implementation to not assign this anymore + * + * Version 1.3 + * - moved registering the observer into startup + * * Version 1.1 + * - added startup event, to make sure API is ready as soon as the add-on is starting + * NOTE: This requires to add the startup event to the manifest, see: + * https://github.com/thundernest/addon-developer-support/tree/master/auxiliary-apis/NotifyTools#usage * * Author: John Bieling (john@thunderbird.net) * @@ -11,28 +20,62 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -// Get various parts of the WebExtension framework that we need. -var { ExtensionCommon } = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm"); -var { ExtensionSupport } = ChromeUtils.import("resource:///modules/ExtensionSupport.jsm"); -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +"use strict"; + +(function (exports) { + + // Get various parts of the WebExtension framework that we need. + var { ExtensionCommon } = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm"); + var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + + var observerTracker = new Set(); + + class NotifyTools extends ExtensionCommon.ExtensionAPI { + getAPI(context) { + return { + NotifyTools: { + + notifyExperiment(data) { + return new Promise(resolve => { + Services.obs.notifyObservers( + { data, resolve }, + "NotifyExperimentObserver", + context.extension.id + ); + }); + }, + + onNotifyBackground: new ExtensionCommon.EventManager({ + context, + name: "NotifyTools.onNotifyBackground", + register: (fire) => { + observerTracker.add(fire.sync); + return () => { + observerTracker.delete(fire.sync); + }; + }, + }).api(), -var NotifyTools = class extends ExtensionCommon.ExtensionAPI { - getAPI(context) { - var self = this; + } + }; + } - this.onNotifyBackgroundObserver = { - observe: async function (aSubject, aTopic, aData) { + // Force API to run at startup, otherwise event listeners might not be added at the requested time. Also needs + // "events": ["startup"] in the experiment manifest + onStartup() { + this.onNotifyBackgroundObserver = async (aSubject, aTopic, aData) => { if ( - Object.keys(self.observerTracker).length > 0 && - aData == self.extension.id + observerTracker.size > 0 && + aData == this.extension.id ) { let payload = aSubject.wrappedJSObject; - // This is called from the BL observer.js and therefore it should have a resolve - // payload, but better check. + + // Make sure payload has a resolve function, which we use to resolve the + // observer notification. if (payload.resolve) { let observerTrackerPromises = []; // Push listener into promise array, so they can run in parallel - for (let listener of Object.values(self.observerTracker)) { + for (let listener of observerTracker.values()) { observerTrackerPromises.push(listener(payload.data)); } // We still have to await all of them but wait time is just the time needed @@ -54,70 +97,39 @@ var NotifyTools = class extends ExtensionCommon.ExtensionAPI { payload.resolve(results[0]); } } else { - // Just call the listener. - for (let listener of Object.values(self.observerTracker)) { + // Older version of NotifyTools, which is not sending a resolve function, deprecated. + console.log("Please update the notifyTools API and the notifyTools script to at least v1.5"); + for (let listener of observerTracker.values()) { listener(payload.data); } } } - }, - }; - - this.observerTracker = {}; - this.observerTrackerNext = 1; - // Add observer for notifyTools.js - Services.obs.addObserver( - this.onNotifyBackgroundObserver, - "NotifyBackgroundObserver", - false - ); - - return { - NotifyTools: { - - notifyExperiment(data) { - return new Promise(resolve => { - Services.obs.notifyObservers( - { data, resolve }, - "NotifyExperimentObserver", - self.extension.id - ); - }); - }, - - onNotifyBackground: new ExtensionCommon.EventManager({ - context, - name: "NotifyTools.onNotifyBackground", - register: (fire) => { - let trackerId = self.observerTrackerNext++; - self.observerTracker[trackerId] = fire.sync; - return () => { - delete self.observerTracker[trackerId]; - }; - }, - }).api(), + }; - } - }; - } + // Add observer for notifyTools.js + Services.obs.addObserver( + this.onNotifyBackgroundObserver, + "NotifyBackgroundObserver", + false + ); + } - // Force API to run at startup, otherwise event listeners might not be added at the requested time. Also needs - // "events": ["startup"] in the experiment manifest + onShutdown(isAppShutdown) { + if (isAppShutdown) { + return; // the application gets unloaded anyway + } - onStartup() { } + // Remove observer for notifyTools.js + Services.obs.removeObserver( + this.onNotifyBackgroundObserver, + "NotifyBackgroundObserver" + ); - onShutdown(isAppShutdown) { - if (isAppShutdown) { - return; // the application gets unloaded anyway + // Flush all caches + Services.obs.notifyObservers(null, "startupcache-invalidate"); } + }; - // Remove observer for notifyTools.js - Services.obs.removeObserver( - this.onNotifyBackgroundObserver, - "NotifyBackgroundObserver" - ); + exports.NotifyTools = NotifyTools; - // Flush all caches - Services.obs.notifyObservers(null, "startupcache-invalidate"); - } -}; +})(this) diff --git a/api/WindowListener/CHANGELOG.md b/api/WindowListener/CHANGELOG.md index 7b887c4..fc7d848 100644 --- a/api/WindowListener/CHANGELOG.md +++ b/api/WindowListener/CHANGELOG.md @@ -1,3 +1,7 @@ +Version: 1.57 +------------- +- fix race condition which could prevent the AOM tab to be monkey patched correctly + Version: 1.56 ------------- - be precise on which revision the wrench symbol should be displayed, instead of diff --git a/api/WindowListener/implementation.js b/api/WindowListener/implementation.js index 95876b4..822a3d3 100644 --- a/api/WindowListener/implementation.js +++ b/api/WindowListener/implementation.js @@ -2,7 +2,7 @@ * This file is provided by the addon-developer-support repository at * https://github.com/thundernest/addon-developer-support * - * Version: 1.56 + * Version: 1.57 * * Author: John Bieling (john@thunderbird.net) * @@ -200,7 +200,7 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { // returns the outer browser, not the nested browser of the add-on manager // events must be attached to the outer browser getAddonManagerFromTab(tab) { - if (tab.browser) { + if (tab.browser && tab.mode.name == "contentTab") { let win = tab.browser.contentWindow; if (win && win.location.href == "about:addons") { return win; @@ -211,9 +211,28 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { getAddonManagerFromWindow(window) { let tabMail = this.getTabMail(window); for (let tab of tabMail.tabInfo) { - let win = this.getAddonManagerFromTab(tab); - if (win) { - return win; + let managerWindow = this.getAddonManagerFromTab(tab); + if (managerWindow) { + return managerWindow; + } + } + } + + async getAddonManagerFromWindowWaitForLoad(window) { + let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane"); + + let tabMail = this.getTabMail(window); + for (let tab of tabMail.tabInfo) { + if (tab.browser && tab.mode.name == "contentTab") { + // Instead of registering a load observer, wait until its loaded. Not nice, + // but gets aroud a lot of edge cases. + while(!tab.pageLoaded) { + await new Promise(r => setTimeout(r, 150)); + } + let managerWindow = this.getAddonManagerFromTab(tab); + if (managerWindow) { + return managerWindow; + } } } } @@ -329,41 +348,20 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { // TabMonitor to detect opening of tabs, to setup the options button in the add-on manager. this.tabMonitor = { - onTabTitleChanged(aTab) { }, - onTabClosing(aTab) { }, - onTabPersist(aTab) { }, - onTabRestored(aTab) { }, - onTabSwitched(aNewTab, aOldTab) { - //self.setupAddonManager(self.getAddonManagerFromTab(aNewTab)); - }, - async onTabOpened(aTab) { - if (aTab.browser) { - if (!aTab.pageLoaded) { - // await a location change if browser is not loaded yet - await new Promise((resolve) => { - let reporterListener = { - QueryInterface: ChromeUtils.generateQI([ - "nsIWebProgressListener", - "nsISupportsWeakReference", - ]), - onStateChange() { }, - onProgressChange() { }, - onLocationChange( - /* in nsIWebProgress*/ aWebProgress, - /* in nsIRequest*/ aRequest, - /* in nsIURI*/ aLocation - ) { - aTab.browser.removeProgressListener(reporterListener); - resolve(); - }, - onStatusChange() { }, - onSecurityChange() { }, - onContentBlockingEvent() { }, - }; - aTab.browser.addProgressListener(reporterListener); - }); + onTabTitleChanged(tab) { }, + onTabClosing(tab) { }, + onTabPersist(tab) { }, + onTabRestored(tab) { }, + onTabSwitched(aNewTab, aOldTab) { }, + async onTabOpened(tab) { + if (tab.browser && tab.mode.name == "contentTab") { + let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane"); + // Instead of registering a load observer, wait until its loaded. Not nice, + // but gets aroud a lot of edge cases. + while(!tab.pageLoaded) { + await new Promise(r => setTimeout(r, 150)); } - self.setupAddonManager(self.getAddonManagerFromTab(aTab)); + self.setupAddonManager(self.getAddonManagerFromTab(tab)); } }, }; @@ -594,16 +592,14 @@ var WindowListener = class extends ExtensionCommon.ExtensionAPI { self ); } else { - // Setup the options button/menu in the add-on manager, if it is already open. - self.setupAddonManager( - self.getAddonManagerFromWindow(window), - true - ); // Add a tabmonitor, to be able to setup the options button/menu in the add-on manager. self .getTabMail(window) .registerTabMonitor(self.tabMonitor); window[self.uniqueRandomID].hasTabMonitor = true; + // Setup the options button/menu in the add-on manager, if it is already open. + let managerWindow = await self.getAddonManagerFromWindowWaitForLoad(window); + self.setupAddonManager(managerWindow, true); } } } diff --git a/background.js b/background.js index 28f2564..77bc91d 100644 --- a/background.js +++ b/background.js @@ -9,13 +9,13 @@ async function main() { // Handle request from preferences.js. See // https://github.com/thundernest/addon-developer-support/tree/master/scripts/preferences/ // The localStorageHandler also takes care of default prefs. - localStorageHandler.init(defaults); - localStorageHandler.enableListeners(); + await localStorageHandler.init(defaults); + await localStorageHandler.enableListeners(); // Test if our localStorageHandler is working properly. for (let pref of Object.keys(defaults)) { - console.log(`MoreSnooze preferences are: ${pref} = ${await localStorageHandler.getPref(pref)}`); - }; + console.log(`MoreSnooze: Preferences are ${pref} = ${await localStorageHandler.getPref(pref)}`); + } messenger.WindowListener.registerChromeUrl([ ["content", "moresnooze", "content/" ], @@ -23,7 +23,7 @@ async function main() { ]); messenger.WindowListener.registerWindow("chrome://calendar/content/calendar-alarm-dialog.xhtml", "chrome://moresnooze/content/moresnooze.js"); - messenger.WindowListener.startListening(); + await messenger.WindowListener.startListening(); } main(); diff --git a/content/moresnooze.js b/content/moresnooze.js index 42bdeb7..7a93f42 100644 --- a/content/moresnooze.js +++ b/content/moresnooze.js @@ -38,9 +38,19 @@ /* global document, MutationObserver, Components, MozXULElement */ 'use strict'; -Services.scriptloader.loadSubScript("chrome://moresnooze/content/notifyTools.js", window, "UTF-8"); -Services.scriptloader.loadSubScript("chrome://moresnooze/content/preferences.js", window, "UTF-8"); -Services.scriptloader.loadSubScript("chrome://moresnooze/content/fields.js", window, "UTF-8"); + +const ADDON_ID = "moresnooze@cyrille.nocus"; + +let {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +let {ExtensionParent} = ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm"); + +let extension = ExtensionParent.GlobalManager.getExtension(ADDON_ID); + +window.moreSnooze = {}; + +Services.scriptloader.loadSubScript(extension.rootURI.resolve("content/notifyTools.js"), window.moreSnooze, "UTF-8"); +Services.scriptloader.loadSubScript(extension.rootURI.resolve("content/preferences.js"), window.moreSnooze, "UTF-8"); +Services.scriptloader.loadSubScript(extension.rootURI.resolve("content/fields.js"), window.moreSnooze, "UTF-8"); function newMenuItem(item) { return ( @@ -51,9 +61,11 @@ function newMenuItem(item) { } function buildCustomSnoozeMenus() { - const selectedPrefs = window.fieldList.filter(pref => window.preferences.getPref(pref.name)); + const selectedPrefs = window.moreSnooze.fieldList.filter(pref => window.moreSnooze.preferences.getPref(pref.name)); const menus = document.querySelectorAll('[is="calendar-snooze-popup"]'); + console.log("MoreSnooze: Patch popups.") + menus.forEach((menu) => { const items = menu.querySelectorAll('menuitem:not(.unit-menuitem)'); items.forEach((item) => { item.parentNode.removeChild(item); }); @@ -64,34 +76,54 @@ function buildCustomSnoozeMenus() { }); } -const mutationObserver = new window.MutationObserver(function(mutations) { - mutations.forEach(function(mutation) { - if (mutation.attributeName === 'title') buildCustomSnoozeMenus(); - }); -}); +function mutationCallback(mutations) { + if (mutations.length > 0) { + buildCustomSnoozeMenus(); + } +} + +// Resolution of bug https://bugzilla.mozilla.org/show_bug.cgi?id=1703164 changed the dialog from +// to ...</...>. To support both notations, we listen both for +// changes of the title attribute of the dialog tag and for changes of the title tag. +const mutationObserverOld = new window.MutationObserver(mutationCallback); +const mutationObserverNew = new window.MutationObserver(mutationCallback); async function onLoad(activatedWhileWindowOpen) { - // NotfyTools usually get enabled automatically, but not in when overlayed. - // They enable is needed by the preference script to get notified on pref - // changes by the background script. - window.notifyTools.enable(); - await window.preferences.initCache(); - window.preferences.registerOnChangeListener(buildCustomSnoozeMenus); + console.log("MoreSnooze: Loading."); + + window.moreSnooze.notifyTools.setAddOnId(ADDON_ID); + await window.moreSnooze.preferences.initCache(); + window.moreSnooze.preferences.registerOnChangeListener(function () { + console.log("MoreSnooze: Preferences changed.") + buildCustomSnoozeMenus(); + }); buildCustomSnoozeMenus(); - mutationObserver.observe( + + mutationObserverOld.observe( document.querySelector('#calendar-alarm-dialog'), - { attributes: true } + { attributes: true, attributeFilter: ['title'] } ); + mutationObserverNew.observe( + document.querySelector('#calendar-alarm-dialog head title'), + { childList: true } + ); + + console.log("MoreSnooze: Loaded."); } function onUnload(deactivatedWhileWindowOpen) { + console.log("MoreSnooze: Unloading."); + // Cleaning up the window UI is only needed when the // add-on is being deactivated/removed while the window // is still open. It can be skipped otherwise. if (!deactivatedWhileWindowOpen) { return; } - window.notifyTools.disable(); - mutationObserver.disconnect(); + window.moreSnooze.notifyTools.removeAllListeners(); + mutationObserverOld.disconnect(); + mutationObserverNew.disconnect(); + + console.log("MoreSnooze: Unloaded."); } \ No newline at end of file diff --git a/content/notifyTools.js b/content/notifyTools.js index 95d37dc..b9bb636 100644 --- a/content/notifyTools.js +++ b/content/notifyTools.js @@ -1,5 +1,5 @@ -// Set this to the ID of your add-on. -var ADDON_ID = "moresnooze@cyrille.nocus"; +// Set this to the ID of your add-on, or call notifyTools.setAddonID(). +var ADDON_ID = ""; /* * This file is provided by the addon-developer-support repository at @@ -8,6 +8,13 @@ var ADDON_ID = "moresnooze@cyrille.nocus"; * For usage descriptions, please check: * https://github.com/thundernest/addon-developer-support/tree/master/scripts/notifyTools * + * Version: 1.5 + * - deprecate enable(), disable() and registerListener() + * - add setAddOnId() + * + * Version: 1.4 + * - auto enable/disable + * * Version: 1.3 * - registered listeners for notifyExperiment can return a value * - remove WindowListener from name of observer @@ -24,16 +31,24 @@ var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); var notifyTools = { registeredCallbacks: {}, registeredCallbacksNextId: 1, + addOnId: ADDON_ID, + + setAddOnId: function (addOnId) { + this.addOnId = addOnId; + }, onNotifyExperimentObserver: { observe: async function (aSubject, aTopic, aData) { - if (ADDON_ID == "") { + if (notifyTools.addOnId == "") { throw new Error("notifyTools: ADDON_ID is empty!"); } - if (aData != ADDON_ID) { + if (aData != notifyTools.addOnId) { return; } let payload = aSubject.wrappedJSObject; + + // Make sure payload has a resolve function, which we use to resolve the + // observer notification. if (payload.resolve) { let observerTrackerPromises = []; // Push listener into promise array, so they can run in parallel @@ -61,7 +76,8 @@ var notifyTools = { payload.resolve(results[0]); } } else { - // Just call the listener. + // Older version of NotifyTools, which is not sending a resolve function, deprecated. + console.log("Please update the notifyTools API to at least v1.5"); for (let registeredCallback of Object.values( notifyTools.registeredCallbacks )) { @@ -71,7 +87,15 @@ var notifyTools = { }, }, - registerListener: function (listener) { + addListener: function (listener) { + if (Object.values(this.registeredCallbacks).length == 0) { + Services.obs.addObserver( + this.onNotifyExperimentObserver, + "NotifyExperimentObserver", + false + ); + } + let id = this.registeredCallbacksNextId++; this.registeredCallbacks[id] = listener; return id; @@ -79,50 +103,61 @@ var notifyTools = { removeListener: function (id) { delete this.registeredCallbacks[id]; + if (Object.values(this.registeredCallbacks).length == 0) { + Services.obs.removeObserver( + this.onNotifyExperimentObserver, + "NotifyExperimentObserver" + ); + } + }, + + removeAllListeners: function () { + if (Object.values(this.registeredCallbacks).length != 0) { + Services.obs.removeObserver( + this.onNotifyExperimentObserver, + "NotifyExperimentObserver" + ); + } + this.registeredCallbacks = {}; }, notifyBackground: function (data) { - if (ADDON_ID == "") { + if (this.addOnId == "") { throw new Error("notifyTools: ADDON_ID is empty!"); } return new Promise((resolve) => { Services.obs.notifyObservers( { data, resolve }, "NotifyBackgroundObserver", - ADDON_ID + this.addOnId ); }); }, + + // Deprecated. + enable: function () { - Services.obs.addObserver( - this.onNotifyExperimentObserver, - "NotifyExperimentObserver", - false - ); + console.log("Manually calling notifyTools.enable() is no longer needed."); }, disable: function () { - Services.obs.removeObserver( - this.onNotifyExperimentObserver, - "NotifyExperimentObserver" - ); + console.log("notifyTools.disable() has been deprecated, use notifyTools.removeAllListeners() instead."); + this.removeAllListeners(); }, -}; + registerListener: function (listener) { + console.log("notifyTools.registerListener() has been deprecated, use notifyTools.addListener() instead."); + this.addListener(listener); + }, + +}; if (typeof window != "undefined" && window) { window.addEventListener( - "load", + "unload", function (event) { - notifyTools.enable(); - window.addEventListener( - "unload", - function (event) { - notifyTools.disable(); - }, - false - ); + notifyTools.removeAllListeners(); }, false ); diff --git a/manifest.json b/manifest.json index 168e778..6ef7f27 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extensionName__", "description": "__MSG_extensionDescription__", - "version": "3.2", + "version": "3.3", "author": "Martin Pecka", "homepage_url": "https://github.com/peci1/more-snooze", "default_locale": "en", @@ -30,7 +30,8 @@ "parent": { "scopes": ["addon_parent"], "paths": [["NotifyTools"]], - "script": "api/NotifyTools/implementation.js" + "script": "api/NotifyTools/implementation.js", + "events": ["startup"] } } },