Background HTML
- - diff --git a/webext/background.js b/webext/background.js index 54f6354..a79ee70 100644 --- a/webext/background.js +++ b/webext/background.js @@ -1,11 +1,6 @@ var SysTrayX = { startupState: undefined, - pollTiming: { - pollStartupDelay: "60", - pollInterval: "60", - }, - platformInfo: undefined, browserInfo: undefined, @@ -15,11 +10,11 @@ var SysTrayX = { SysTrayX.Messaging = { accounts: [], + countType: 0, + filtersExt: undefined, + filters: undefined, init: function () { - // Get the filters from the storage - SysTrayX.Messaging.getFilters(); - // Lookout for storage changes browser.storage.onChanged.addListener(SysTrayX.Messaging.storageChanged); @@ -38,35 +33,70 @@ SysTrayX.Messaging = { // Send preferences to app SysTrayX.Messaging.sendPreferences(); - // Start polling the accounts - window.setTimeout( - SysTrayX.Messaging.pollAccounts, - SysTrayX.pollTiming.pollStartupDelay * 1000 - ); + /* + // New mail listener (TB76+) + if (SysTrayX.browserInfo.majorVersion > 75) { + // + // Mixed results, forgets accounts?, double events? + // + browser.messages.onNewMailReceived.addListener( + SysTrayX.Messaging.newMail + ); + } + */ + + browser.folderChange.onUnreadMailChange.addListener(function (unread) { + console.debug("folderChangeListener: " + unread); + + SysTrayX.Messaging.unreadCb(unread); + }); + + // Set the count type in the folderChange listener + browser.folderChange.setCountType(Number(SysTrayX.Messaging.countType)); + + // Set the filters in the folderChange listener + browser.folderChange.setFilters(SysTrayX.Messaging.filtersExt); // Try to catch the window state browser.windows.onFocusChanged.addListener(SysTrayX.Window.focusChanged); }, + /* + newMail: async function (folder, messages) { + console.debug( + "New mail: " + folder.accountId + ", " + messages.messages.length + ); + + let unread = messages.messages.length; + while (messages.id) { + page = await browser.messages.continueList(messages.id); + + unread = unread + page.messages.length; + } + console.debug("Unread: " + unread); + }, + */ + // // Handle a storage change // storageChanged: function (changes, area) { // Get the new preferences - SysTrayX.Messaging.getFilters(); - if ("pollStartupDelay" in changes && changes["pollStartupDelay"].newValue) { - SysTrayX.pollTiming = { - ...SysTrayX.pollTiming, - pollStartupDelay: changes["pollStartupDelay"].newValue, - }; + if ("filtersExt" in changes && changes["filtersExt"].newValue) { + SysTrayX.Messaging.filtersExt = changes["filtersExt"].newValue; + + browser.folderChange.setFilters(SysTrayX.Messaging.filtersExt); } - if ("pollInterval" in changes && changes["pollInterval"].newValue) { - SysTrayX.pollTiming = { - ...SysTrayX.pollTiming, - pollInterval: changes["pollInterval"].newValue, - }; + if ("filters" in changes && changes["filters"].newValue) { + SysTrayX.Messaging.filters = changes["filters"].newValue; + } + + if ("countType" in changes && changes["countType"].newValue) { + SysTrayX.Messaging.countType = changes["countType"].newValue; + + browser.folderChange.setCountType(Number(SysTrayX.Messaging.countType)); } if ("addonprefchanged" in changes && changes["addonprefchanged"].newValue) { @@ -82,86 +112,6 @@ SysTrayX.Messaging = { } }, - // - // Poll the accounts - // - pollAccounts: function () { - // - // Get the unread nessages of the selected accounts - // - const filtersDiv = document.getElementById("filters"); - const filtersAttr = filtersDiv.getAttribute("data-filters"); - - if (filtersAttr !== "undefined") { - const filters = JSON.parse(filtersAttr); - - if (filters.length > 0) { - SysTrayX.Messaging.unReadMessages(filters).then( - SysTrayX.Messaging.unreadCb - ); - } else { - SysTrayX.Link.postSysTrayXMessage({ unreadMail: 0 }); - } - } else { - // Never saved anything, construct temporary filters - if (SysTrayX.Messaging.accounts.length > 0) { - // Construct inbox filters for all accounts - let filters = []; - SysTrayX.Messaging.accounts.forEach((account) => { - const inbox = account.folders.filter( - (folder) => folder.type == "inbox" - ); - - if (inbox.length > 0) { - filters.push({ - unread: true, - folder: inbox[0], - }); - } - }); - - // Store them in the background HTML - const filtersDiv = document.getElementById("filters"); - filtersDiv.setAttribute("data-filters", JSON.stringify(filters)); - - SysTrayX.Messaging.unReadMessages(filters).then( - SysTrayX.Messaging.unreadCb - ); - } else { - // No accounts, no mail - SysTrayX.Link.postSysTrayXMessage({ unreadMail: 0 }); - } - } - - // Next round... - window.setTimeout( - SysTrayX.Messaging.pollAccounts, - SysTrayX.pollTiming.pollInterval * 1000 - ); - }, - - // - // Use the messages API to get the unread messages (Promise) - // Be aware that the data is only avaiable inside the callback - // - unReadMessages: async function (filters) { - let unreadMessages = 0; - for (let i = 0; i < filters.length; ++i) { - let page = await browser.messages.query(filters[i]); - let unread = page.messages.length; - - while (page.id) { - page = await browser.messages.continueList(page.id); - - unread = unread + page.messages.length; - } - - unreadMessages = unreadMessages + unread; - } - - return unreadMessages; - }, - // // Callback for unReadMessages // @@ -191,8 +141,6 @@ SysTrayX.Messaging = { sendPreferences: function () { const getter = browser.storage.sync.get([ "debug", - "pollStartupDelay", - "pollInterval", "minimizeType", "startMinimized", "iconType", @@ -200,14 +148,13 @@ SysTrayX.Messaging = { "icon", "showNumber", "numberColor", + "countType", ]); getter.then(this.sendPreferencesStorage, this.onSendPreferecesStorageError); }, sendPreferencesStorage: function (result) { const debug = result.debug || "false"; - const pollStartupDelay = result.pollStartupDelay || "60"; - const pollInterval = result.pollInterval || "60"; const minimizeType = result.minimizeType || "1"; const startMinimized = result.startMinimized || "false"; const iconType = result.iconType || "0"; @@ -215,13 +162,12 @@ SysTrayX.Messaging = { const icon = result.icon || []; const showNumber = result.showNumber || "true"; const numberColor = result.numberColor || "#000000"; + const countType = result.countType || "0"; // Send it to the app SysTrayX.Link.postSysTrayXMessage({ preferences: { debug: debug, - pollStartupDelay: pollStartupDelay, - pollInterval: pollInterval, minimizeType: minimizeType, startMinimized: startMinimized, iconType: iconType, @@ -229,6 +175,7 @@ SysTrayX.Messaging = { icon: icon, showNumber: showNumber, numberColor: numberColor, + countType: countType, }, }); @@ -245,26 +192,6 @@ SysTrayX.Messaging = { console.log(`GetIcon Error: ${error}`); }, - // - // Get the filters from the storage - // - getFilters: function () { - const getter = browser.storage.sync.get("filters"); - getter.then(this.getFiltersStorage, this.onGetFiltersStorageError); - }, - - // - // Get the filters from the storage and - // make them available in the background HTML - // - getFiltersStorage: function (result) { - const filters = result.filters || undefined; - - // Store them in the background HTML - const filtersDiv = document.getElementById("filters"); - filtersDiv.setAttribute("data-filters", JSON.stringify(filters)); - }, - onGetAccountsStorageError: function (error) { console.log(`GetAccounts Error: ${error}`); }, @@ -348,6 +275,13 @@ SysTrayX.Link = { }); } + const countType = response["preferences"].countType; + if (countType) { + browser.storage.sync.set({ + countType: countType, + }); + } + const minimizeType = response["preferences"].minimizeType; if (minimizeType) { browser.storage.sync.set({ @@ -362,20 +296,6 @@ SysTrayX.Link = { }); } - const pollStartupDelay = response["preferences"].pollStartupDelay; - if (pollStartupDelay) { - browser.storage.sync.set({ - pollStartupDelay: pollStartupDelay, - }); - } - - const pollInterval = response["preferences"].pollInterval; - if (pollInterval) { - browser.storage.sync.set({ - pollInterval: pollInterval, - }); - } - const debug = response["preferences"].debug; if (debug) { browser.storage.sync.set({ @@ -408,9 +328,6 @@ async function start() { SysTrayX.startupState = state; - // Get the poll timing - SysTrayX.pollTiming = await getPollTiming(); - // Set platform SysTrayX.platformInfo = await browser.runtime .getPlatformInfo() @@ -430,6 +347,10 @@ async function start() { .getBrowserInfo() .then((info) => info); + const version = SysTrayX.browserInfo.version.split("."); + SysTrayX.browserInfo.majorVersion = version[0]; + SysTrayX.browserInfo.minorVersion = version[1]; + console.log("Browser: " + SysTrayX.browserInfo.name); console.log("Vendor: " + SysTrayX.browserInfo.vendor); console.log("Version: " + SysTrayX.browserInfo.version); @@ -454,6 +375,15 @@ async function start() { // Get all accounts SysTrayX.Messaging.accounts = await browser.accounts.list(); + // Get the extended filters + SysTrayX.Messaging.filtersExt = await getFiltersExt(); + + // Get the filters + SysTrayX.Messaging.filters = await getFilters(); + + // Get the count type + SysTrayX.Messaging.countType = await getCountType(); + // Setup the link first SysTrayX.Link.init(); diff --git a/webext/js/defaults.js b/webext/js/defaults.js index 285796c..211d3e8 100644 --- a/webext/js/defaults.js +++ b/webext/js/defaults.js @@ -15,11 +15,11 @@ async function getDefaultIcon() { const iconStored = await getIcon.then(getStoredIcon, onStoredIconError); if (!iconStored) { - const toDataURL = url => + const toDataURL = (url) => fetch(url) - .then(response => response.blob()) + .then((response) => response.blob()) .then( - blob => + (blob) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result); @@ -30,11 +30,8 @@ async function getDefaultIcon() { // Convert image to storage param let { iconMime, iconBase64 } = await toDataURL("icons/blank-icon.png").then( - dataUrl => { - const data = dataUrl - .split(":") - .pop() - .split(","); + (dataUrl) => { + const data = dataUrl.split(":").pop().split(","); return { iconMime: data[0].split(";")[0], iconBase64: data[1] }; } ); @@ -42,7 +39,7 @@ async function getDefaultIcon() { // Store default icon (base64) browser.storage.sync.set({ iconMime: iconMime, - icon: iconBase64 + icon: iconBase64, }); // Store in HTML @@ -69,19 +66,49 @@ async function getStartupState() { } // -// Get poll timing +// Get filters // -async function getPollTiming() { - function getDelayAndInterval(result) { - return { pollStartupDelay: result.pollStartupDelay || "60", pollInterval: result.pollInterval || "60" }; +async function getFilters() { + function getFiltersCb(result) { + return result.filters || undefined; } - function onDelayAndIntervalError() { - return { pollStartupDelay: "60", pollInterval: "60" }; + function onFiltersError() { + return undefined; } - const getTiming = browser.storage.sync.get([ - "pollStartupDelay", - "pollInterval"]); - return await getTiming.then(getDelayAndInterval, onDelayAndIntervalError); + const getFilters = browser.storage.sync.get("filters"); + return await getFilters.then(getFiltersCb, onFiltersError); +} + +// +// Get extended filters +// +async function getFiltersExt() { + function getFiltersExtCb(result) { + return result.filtersExt || undefined; + } + + function onFiltersExtError() { + return undefined; + } + + const getFiltersExt = browser.storage.sync.get("filtersExt"); + return await getFiltersExt.then(getFiltersExtCb, onFiltersExtError); +} + +// +// Get count type +// +async function getCountType() { + function getCountTypeCb(result) { + return result.countType || "0"; + } + + function onCountTypeError() { + return undefined; + } + + const getCountType = browser.storage.sync.get("countType"); + return await getCountType.then(getCountTypeCb, onCountTypeError); } diff --git a/webext/js/folderChange.js b/webext/js/folderChange.js new file mode 100644 index 0000000..79b051e --- /dev/null +++ b/webext/js/folderChange.js @@ -0,0 +1,416 @@ +/* eslint-disable object-shorthand */ + +const Ci = Components.interfaces; + +// Get various parts of the WebExtension framework that we need. +var { ExtensionCommon } = ChromeUtils.import( + "resource://gre/modules/ExtensionCommon.jsm" +); + +// You probably already know what this does. +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { fixIterator } = ChromeUtils.import( + "resource:///modules/iteratorUtils.jsm" +); + +// ChromeUtils.import() works in experiments for core resource urls as it did +// in legacy add-ons. However, chrome:// urls that point to add-on resources no +// longer work, as the "chrome.manifest" file is no longer supported, which +// defined the root path for each add-on. Instead, ChromeUtils.import() needs +// a url generated by +// +// let url = context.extension.rootURI.resolve("path/to/file.jsm") +// +// Instead of taking the extension object from the context, you may generate +// the extension object from a given add-on ID as shown in the example below. +// This allows to import a JSM without context, for example inside another JSM. +// +var { ExtensionParent } = ChromeUtils.import( + "resource://gre/modules/ExtensionParent.jsm" +); +var extension = ExtensionParent.GlobalManager.getExtension( + "systray-x@Ximi1970" +); +var { folderChange } = ChromeUtils.import( + extension.rootURI.resolve("modules/folderChange.jsm") +); + +// This is the important part. It implements the functions and events defined in schema.json. +// The variable must have the same name you've been using so far, "myapi" in this case. +var folderChange = class extends ExtensionCommon.ExtensionAPI { + getAPI(context) { + console.log("folderChange module started"); + + // To be notified of the extension going away, call callOnClose with any object that has a + // close function, such as this one. + context.callOnClose(this); + + // + // Setup folder listener + // + SysTrayX.init(); + + return { + // Again, this key must have the same name. + folderChange: { + setCountType: async function (type) { + SysTrayX.setCountType(type); + }, + + setFilters: async function (filters) { + SysTrayX.setFilters(filters); + }, + + onUnreadMailChange: new ExtensionCommon.EventManager({ + context, + name: "folderChange.onUnreadMailChange", + // In this function we add listeners for any events we want to listen to, and return a + // function that removes those listeners. To have the event fire in your extension, + // call fire.async. + register(fire) { + function callback(event, unread) { + return fire.async(unread); + } + + SysTrayX.add(callback); + return function () { + SysTrayX.remove(callback); + }; + }, + }).api(), + }, + }; + } + + close() { + /* + * Remove the folder listener + */ + SysTrayX.shutdown(); + + // This function is called if the extension is disabled or removed, or Thunderbird closes. + // We registered it with callOnClose, above. + console.log("folderChange module closed"); + + // Unload the JSM we imported above. This will cause Thunderbird to forget about the JSM, and + // load it afresh next time `import` is called. (If you don't call `unload`, Thunderbird will + // remember this version of the module and continue to use it, even if your extension receives + // an update.) You should *always* unload JSMs provided by your extension. + Cu.unload(extension.getURL("modules/folderChange.jsm")); + } +}; + +// A helpful class for listening to windows opening and closing. +// (This file had a lowercase E in Thunderbird 65 and earlier.) +var { ExtensionSupport } = ChromeUtils.import( + "resource:///modules/ExtensionSupport.jsm" +); + +var SysTrayX = { + MESSAGE_COUNT_TYPE_UNREAD: 0, + MESSAGE_COUNT_TYPE_NEW: 1, + + countType: this.MESSAGE_COUNT_TYPE_UNREAD, + + initialized: false, + + accounts: undefined, + filters: undefined, + + currentMsgCount: null, + newMsgCount: null, + + callback: undefined, + + init: function () { + if (this.initialized) { + console.warn("Folder listener already initialized"); + return; + } + + console.log("Initializing folder listener"); + + // Get the mail accounts using MailServices + this.getAccounts(); + + // Start listener + MailServices.mailSession.AddFolderListener( + this.mailSessionListener, + this.mailSessionListener.notificationFlags + ); + + this.initialized = true; + }, + + shutdown: function () { + if (!this.initialized) { + return; + } + + log.log("Shutting down folder listener"); + + // Stop listener + MailServices.mailSession.RemoveFolderListener(this.mailSessionListener); + + this.initialized = false; + }, + + setCountType: function (type) { + console.debug("setCountType: " + type); + + if (type === 0) { + this.countType = this.MESSAGE_COUNT_TYPE_UNREAD; + } else if (type === 1) { + this.countType = this.MESSAGE_COUNT_TYPE_NEW; + } else console.log("Unknown count type: " + type); + + // Update count + this.updateMsgCountWithCb(); + }, + + setFilters: function (filters) { + this.filters = filters; + + // Update count + this.updateMsgCountWithCb(); + }, + + mailSessionListener: { + notificationFlags: + Ci.nsIFolderListener.propertyFlagChanged | + Ci.nsIFolderListener.boolPropertyChanged | + Ci.nsIFolderListener.intPropertyChanged, + + OnItemIntPropertyChanged(item, property, oldValue, newValue) { + // TotalUnreadMessages, BiffState (per server) + /* + console.debug( + "OnItemIntPropertyChanged " + + property + + " for folder " + + item.prettyName + + " was " + + oldValue + + " became " + + newValue + + " NEW MESSAGES=" + + item.getNumNewMessages(true) + ); + */ + this.onMsgCountChange(item, property, oldValue, newValue); + }, + + OnItemBoolPropertyChanged: function (item, property, oldValue, newValue) { + // NewMessages (per folder) + /* + console.debug( + "OnItemBoolPropertyChanged " + + property + + " for folder " + + item.prettyName + + " was " + + oldValue + + " became " + + newValue + + " NEW MESSAGES=" + + item.getNumNewMessages(true) + ); + */ + this.onMsgCountChange(item, property, oldValue, newValue); + }, + + OnItemPropertyFlagChanged: function (item, property, oldFlag, newFlag) { + /* + console.debug( + "OnItemPropertyFlagChanged" + + property + + " for " + + item + + " was " + + oldFlag + + " became " + + newFlag + ); + */ + this.onMsgCountChange(item, property, oldFlag, newFlag); + }, + + onMsgCountChange: function (item, property, oldValue, newValue) { + let msgCountType = SysTrayX.countType; + + let prop = property.toString(); + if ( + prop === "TotalUnreadMessages" && + msgCountType === SysTrayX.MESSAGE_COUNT_TYPE_UNREAD + ) { + SysTrayX.updateMsgCountWithCb(); + } else { + if ( + prop === "NewMessages" && + msgCountType === SysTrayX.MESSAGE_COUNT_TYPE_NEW + ) { + if (oldValue === true && newValue === false) { + // https://bugzilla.mozilla.org/show_bug.cgi?id=727460 + item.setNumNewMessages(0); + } + SysTrayX.updateMsgCountWithCb(); + } + } + }, + }, + + updateMsgCountWithCb(callback) { + if (callback === undefined || !callback) { + callback = function (currentMsgCount, newMsgCount) { + // default + // .updateIcon(newMsgCount); + console.debug("Update icon: " + newMsgCount); + + if (SysTrayX.callback) { + SysTrayX.callback("unread-changed", newMsgCount); + } + }; + } + + let msgCountType = SysTrayX.countType; + if (msgCountType === SysTrayX.MESSAGE_COUNT_TYPE_UNREAD) { + this.countMessages("UnreadMessages"); + } else if (msgCountType === SysTrayX.MESSAGE_COUNT_TYPE_NEW) { + this.countMessages("HasNewMessages"); + } else console.error("Unknown message count type: " + msgCountType); + + // currentMsgCount and newMsgCount may be integers or bool, which do + // also support comparison operations + callback.call(this, this.currentMsgCount, this.newMsgCount); + this.currentMsgCount = this.newMsgCount; + }, + + countMessages(countType) { + console.debug("countMessages: " + countType); + + this.newMsgCount = 0; + for (let accountServer of this.accounts) { + // if (accountServer.type === ACCOUNT_SERVER_TYPE_IM) { + // continue; + // } + + // if (excludedAccounts.indexOf(accountServer.key) >= 0) + // { + // continue; + // } + + this.applyToSubfolders( + accountServer.prettyName, + accountServer.rootFolder, + true, + function (folder) { + this.msgCountIterate(countType, accountServer.prettyName, folder); + } + ); + } + + console.debug("Total " + countType + " = " + this.newMsgCount); + }, + + applyToSubfolders(account, folder, recursive, fun) { + if (folder.hasSubFolders) { + let subFolders = folder.subFolders; + while (subFolders.hasMoreElements()) { + let subFolder = subFolders.getNext().QueryInterface(Ci.nsIMsgFolder); + if (recursive && subFolder.hasSubFolders) + this.applyToSubfoldersRecursive(account, subFolder, recursive, fun); + else fun.call(this, subFolder); + } + } + }, + + applyToSubfoldersRecursive(account, folder, recursive, fun) { + fun.call(this, folder); + this.applyToSubfolders(account, folder, recursive, fun); + }, + + msgCountIterate(type, account, folder) { + let count = false; + + if (SysTrayX.filters) { + const match = SysTrayX.filters.filter( + (filter) => + filter.folder.accountName === account && + filter.folder.name === folder.prettyName + ); + + count = match.length > 0 + } else { + count = folder.getFlag(Ci.nsMsgFolderFlags.Inbox); + } + + if (count) { + SysTrayX["add" + type](folder); + } + }, + + addUnreadMessages(folder) { + let folderUnreadMsgCount = folder["getNumUnread"](false); + + console.debug( + "folder: " + + folder.prettyName + + " folderUnreadMsgCount= " + + folderUnreadMsgCount + ); + + /* nsMsgDBFolder::GetNumUnread basically returns mNumUnreadMessages + + mNumPendingUnreadMessages, while mNumPendingUnreadMessages may get -1 + when updated from the cache. Which means getNumUnread might return -1. */ + if (folderUnreadMsgCount > 0) { + this.newMsgCount += folderUnreadMsgCount; + } + }, + + addHasNewMessages(folder) { + let folderNewMsgCount = folder.hasNewMessages; + + console.debug( + "folder: " + folder.prettyName + " hasNewMessages= " + folderNewMsgCount + ); + + this.newMsgCount = this.newMsgCount || folderNewMsgCount; + }, + + getAccounts() { + console.debug("getAccounts"); + + let accountServers = []; + for (let accountServer of fixIterator( + MailServices.accounts.accounts, + Ci.nsIMsgAccount + )) { + accountServers.push(accountServer.incomingServer); + } + + for (let i = 0, len = accountServers.length; i < len; ++i) { + console.debug( + "ACCOUNT: " + + accountServers[i].prettyName + + " type: " + + accountServers[i].type + + " key: " + + accountServers[i].key.toString() + ); + } + + // Store the accounts + this.accounts = accountServers; + }, + + add(callback) { + this.callback = callback; + }, + + remove(callback) { + this.callback = undefined; + }, +}; diff --git a/webext/js/options_accounts.js b/webext/js/options_accounts.js index 1d721a8..c978845 100644 --- a/webext/js/options_accounts.js +++ b/webext/js/options_accounts.js @@ -23,7 +23,7 @@ SysTrayX.Accounts = { * Callback for getAccounts */ getAccountsCb: function (mailAccount) { - function createFolderTree(folders) { + function createFolderTreePre74(accountName, folders) { let result = []; let level = { result }; @@ -35,8 +35,9 @@ SysTrayX.Accounts = { if (!r[name]) { r[name] = { result: [] }; r.result.push({ - name: folder.name, + accountName: accountName, accountId: folder.accountId, + name: folder.name, path: folder.path, subFolders: r[name].result, }); @@ -49,6 +50,22 @@ SysTrayX.Accounts = { return result; } + function createFolderTree(accountName, folders) { + function traverse(folders) { + if (!folders) { + return; + } + for (let f of folders) { + f.accountName = accountName; + traverse(f.subFolders); + } + } + + traverse(folders); + + return folders; + } + let accounts = new Object(); for (let i = 0; i < mailAccount.length; i++) { @@ -119,12 +136,20 @@ SysTrayX.Accounts = { ); typeLi.appendChild(typeText); - // Create a usable folder tree| - - | -- - | -
| - - | -- - | -