Version in base suite: 1.4.0-1 Base version: mailmindr_1.4.0-1 Target version: mailmindr_1.7.1-1~deb12u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/m/mailmindr/mailmindr_1.4.0-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/m/mailmindr/mailmindr_1.7.1-1~deb12u1.dsc _locales/de/messages.json | 36 _locales/en/messages.json | 80 + api/messages.js | 147 -- debian/changelog | 31 debian/control | 5 debian/copyright | 4 images/mailmindr-flag--rainbow-shadow.svg | 201 --- images/mailmindr-flag_marker--white.svg | 87 + images/mailmindr-flag_marker.svg | 87 + manifest.json | 48 modules/core-utils.mjs.js | 206 ++- modules/defaults.mjs.js | 37 modules/logger.mjs.js | 47 modules/message-actions.mjs.js | 187 ++ modules/message-utils.mjs.js | 135 +- modules/storage.mjs.js | 2 modules/store/actions/actionTypes.mjs.js | 16 modules/store/actions/actions.mjs.js | 91 + modules/store/actions/executeMindr.mjs.js | 215 +++ modules/store/actions/heartBeat.mjs.js | 49 modules/store/actions/index.mjs.js | 7 modules/store/actions/setReplyReceived.mjs.js | 24 modules/store/actions/showMindrAlert.mjs.js | 105 + modules/store/actions/snoozeMindrs.mjs.js | 29 modules/store/reducers/createOrUpdateDraft.mjs.js | 33 modules/store/reducers/createOrUpdateMindr.mjs.js | 26 modules/store/reducers/index.mjs.js | 216 +++ modules/store/reducers/removeMindr.mjs.js | 52 modules/store/selectors/index.mjs.js | 68 + modules/store/state-manager.mjs.js | 142 ++ modules/string-utils.mjs.js | 2 modules/ui-utils.mjs.js | 136 +- schema.json | 20 scripts/mailmindr-background.js | 1465 +++++++--------------- scripts/mailmindr-message-script.js | 23 views/dialogs/create-mindr/index.css | 27 views/dialogs/create-mindr/index.js | 114 - views/dialogs/mindr-alert/index.js | 22 views/options/index.html | 19 views/options/index.js | 43 views/popups/create-outgoing-mindr/index.css | 173 ++ views/popups/create-outgoing-mindr/index.html | 73 + views/popups/create-outgoing-mindr/index.js | 417 ++++++ views/popups/list-all/index.css | 59 views/popups/list-all/index.html | 19 views/popups/list-all/index.js | 90 + 46 files changed, 3563 insertions(+), 1552 deletions(-) diff -Nru mailmindr-1.4.0/_locales/de/messages.json mailmindr-1.7.1/_locales/de/messages.json --- mailmindr-1.4.0/_locales/de/messages.json 2022-08-12 13:32:08.000000000 +0000 +++ mailmindr-1.7.1/_locales/de/messages.json 2024-08-04 20:14:20.000000000 +0000 @@ -17,6 +17,25 @@ "mailmindrMessageDisplayButton": { "message": "Wiedervorlage" }, + "mailmindrComposeMessageButton": { + "message": "Wiedervorlage setzen" + }, + "mailmindrComposeMessageButton.edit": { + "message": "Wiedervorlage bearbeiten" + }, + "mailmindrComposeMessageButton.detailed": { + "message": "$DATE$ um $TIME$", + "placeholders": { + "date": { + "content": "$1", + "example": "2021-12-24" + }, + "time": { + "content": "$2", + "example": "09:00" + } + } + }, "module.string-utils.chunk.and": { "message": "und" }, @@ -380,6 +399,12 @@ "view.options.default-action-preset.description": { "message": "Voreingestellte Aktion bei der Erstellung einer Wiedervorlage" }, + "view.options.default-reminder-preset.label": { + "message": "Standard-Erinnerung:" + }, + "view.options.default-reminder-preset.description": { + "message": "Voreingestellte Zeit, wann an eine Wiedervorlage im Voraus erinnert werden soll." + }, "view.options.snooze-time.label": { "message": "Schlummerfunktion:", "description": "Default snooze time for alerts" @@ -476,6 +501,9 @@ "view.message-display.notification.button.edit": { "message": "Wiedervorlage bearbeiten" }, + "view.message-display.notification.button.remove": { + "message": "Wiedervorlage entfernen" + }, "view.message-display.notification.button.message": { "message": "Eine Wiedervorlage ist gesetzt: $1" }, @@ -529,7 +557,7 @@ "mailmindr.utils.core.timePair": { "message": "#1 #2" }, - "mailmindr.utils.core.relative.weeks": { + "mailmindr.utils.core.relative.weeks.one": { "message": "in einer Woche;in #1 Wochen" }, "mailmindr.utils.core.relative.days": { @@ -592,5 +620,11 @@ }, "mailmindr.utils.core.remindme.before.no-reminder.other": { "message": "Keine Erinnerung" + }, + "mailmindrShortcut_OpenList": { + "message": "Liste mit den Wiedervorlagen öffnen" + }, + "mailmindrShortcut_SetFollowUp": { + "message": "Wiedervorlage setzen" } } diff -Nru mailmindr-1.4.0/_locales/en/messages.json mailmindr-1.7.1/_locales/en/messages.json --- mailmindr-1.4.0/_locales/en/messages.json 2022-08-12 13:32:08.000000000 +0000 +++ mailmindr-1.7.1/_locales/en/messages.json 2024-08-04 20:14:20.000000000 +0000 @@ -17,6 +17,25 @@ "mailmindrMessageDisplayButton": { "message": "Follow-Up" }, + "mailmindrComposeMessageButton": { + "message": "Set follow-up" + }, + "mailmindrComposeMessageButton.edit": { + "message": "Edit follow-up" + }, + "mailmindrComposeMessageButton.detailed": { + "message": "$DATE$ at $TIME$", + "placeholders": { + "date": { + "content": "$1", + "example": "2021-12-24" + }, + "time": { + "content": "$2", + "example": "09:00" + } + } + }, "module.string-utils.chunk.and": { "message": "and" }, @@ -380,6 +399,12 @@ "view.options.default-action-preset.description": { "message": "Pre-selected action setting when creating a new reminder" }, + "view.options.default-reminder-preset.label": { + "message": "Default reminder:" + }, + "view.options.default-reminder-preset.description": { + "message": "Pre-selected time for when the reminder dialog appears" + }, "view.options.snooze-time.label": { "message": "Snooze time in Minutes:", "description": "Default snooze time for alerts" @@ -476,9 +501,58 @@ "view.message-display.notification.button.edit": { "message": "Edit follow-up" }, + "view.message-display.notification.button.remove": { + "message": "Remove follow-up" + }, "view.message-display.notification.button.message": { "message": "Follow-up is set for $1" }, + + "view.dialog.create-outgoing-mindr.title.create": { + "message": "Create reminder" + }, + "view.dialog.create-outgoing-mindr.title.edit": { + "message": "Edit reminder" + }, + "view.dialog.create-outgoing-mindr.label.subject": { + "message": "Subject:" + }, + "view.dialog.create-outgoing-mindr.label.due": { + "message": "Reply until:" + }, + "view.dialog.create-outgoing-mindr.label.action": { + "message": "Action:" + }, + "view.dialog.create-outgoing-mindr.label.icebox": { + "message": "Move to icebox folder" + }, + "view.dialog.create-outgoing-mindr.label.icebox.placeholder": { + "message": "move to icebox folder: $FOLDERNAME$", + "placeholders": { + "foldername": { + "content": "$1", + "example": "Inbox" + } + } + }, + "view.dialog.create-outgoing-mindr.label.remind-me": { + "message": "Remind me:" + }, + "view.dialog.create-outgoing-mindr.caption.remove-follow-up": { + "message": "Remove follow-up" + }, + "view.dialog.create-outgoing-mindr.caption.create-mindr": { + "message": "Create" + }, + "view.dialog.create-outgoing-mindr.caption.update-mindr": { + "message": "Update" + }, + "view.dialog.create-outgoing-mindr.caption.cancel": { + "message": "Cancel" + }, + "view.dialog.create-outgoing-mindr.header": { + "message": "Set follow-up" + }, "mailmindr.utils.core.plural.beforestart.minutes.one": { "message": "$1 minute before due", "description": "Menu entry, e.g. '1 minute before due'" @@ -592,5 +666,11 @@ }, "mailmindr.utils.core.remindme.before.no-reminder.other": { "message": "No reminder" + }, + "mailmindrShortcut_OpenList": { + "message": "Open follow-up list" + }, + "mailmindrShortcut_SetFollowUp": { + "message": "Set follow-up" } } diff -Nru mailmindr-1.4.0/api/messages.js mailmindr-1.7.1/api/messages.js --- mailmindr-1.4.0/api/messages.js 2022-08-12 13:32:08.000000000 +0000 +++ mailmindr-1.7.1/api/messages.js 1970-01-01 00:00:00.000000000 +0000 @@ -1,147 +0,0 @@ -var { ExtensionCommon } = ChromeUtils.import( - 'resource://gre/modules/ExtensionCommon.jsm' -); - -var { ExtensionParent } = ChromeUtils.import( - 'resource://gre/modules/ExtensionParent.jsm' -); - -var extension = ExtensionParent.GlobalManager.getExtension( - 'mailmindr@arndissler.net' -); - -const { XPCOMUtils } = ChromeUtils.import( - 'resource://gre/modules/XPCOMUtils.jsm' -); - -const { MailUtils } = ChromeUtils.import('resource:///modules/MailUtils.jsm'); - -XPCOMUtils.defineLazyModuleGetters(this, { - MailServices: 'resource:///modules/MailServices.jsm', - Services: 'resource://gre/modules/Services.jsm' - // -}); - -var mailmindrMessagesApi = class extends ExtensionCommon.ExtensionAPI { - getAPI(context) { - return { - // - mailmindrMessagesApi: { - openMessageByMessageHeaderId: async function(headerMessageId) { - const msgId = headerMessageId - .replace('>', '') - .replace('<', '') - .trim(); - - try { - if (MailUtils.openMessageByMessageId) { - MailUtils.openMessageByMessageId(msgId); - } else { - const getFirstMessageHeaderForMessageId = ( - msgId, - startServer - ) => { - console.warn( - `MailUtils.openMessageByMessageId doesn't exist, using fallback to manual traveral` - ); - - const findMsgIdInFolder = (msgId, folder) => { - let msgHdr; - // - if (!folder.isServer) { - msgHdr = folder.msgDatabase.getMsgHdrForMessageID( - msgId - ); - if (msgHdr) { - return msgHdr; - } - } - - // - for (let currentFolder of folder.subFolders) { - msgHdr = findMsgIdInFolder( - msgId, - currentFolder - ); - if (msgHdr) { - return msgHdr; - } - } - return null; - }; - - let allServers = - MailServices.accounts.allServers; - if (startServer) { - allServers = [startServer].concat( - allServers.filter( - s => s.key != startServer.key - ) - ); - } - for (let server of allServers) { - if ( - server && - server.canSearchMessages && - !server.isDeferredTo - ) { - let msgHdr = findMsgIdInFolder( - msgId, - server.rootFolder - ); - if (msgHdr) { - return msgHdr; - } - } - } - return null; - }; - - let msgHdr = null; - if (MailUtils.getMsgHdrForMsgId) { - msgHdr = MailUtils.getMsgHdrForMsgId(msgId); - } else { - console.warn( - `MailUtils.getMsgHdrForMsgId doesn't exist, falling back to manual traversal to find msgHdr` - ); - msgHdr = getFirstMessageHeaderForMessageId( - msgId - ); - } - - if (msgHdr) { - if (MailUtils.displayMessage) { - MailUtils.displayMessage(msgHdr); - } else { - console.error( - `MailUtils.displayMessage doesn't exist, no fallback available (${navigator && - navigator.userAgent})` - ); - } - } else { - console.warn( - `No headers found for msgId: '${msgId}'` - ); - } - } - } catch (e) { - console.error( - '[mailmindr] mailmindrMessagesApi.openMessageByMessageHeaderId: ', - e - ); - } - } - } - }; - } - - onShutdown(isAppShutdown) { - if (isAppShutdown) { - return; - } - - // - - Services.obs.notifyObservers(null, 'startupcache-invalidate', null); - } -}; diff -Nru mailmindr-1.4.0/debian/changelog mailmindr-1.7.1/debian/changelog --- mailmindr-1.4.0/debian/changelog 2022-09-24 18:08:36.000000000 +0000 +++ mailmindr-1.7.1/debian/changelog 2024-09-18 14:00:14.000000000 +0000 @@ -1,3 +1,34 @@ +mailmindr (1.7.1-1~deb12u1) bookworm; urgency=medium + + * Prepared for bookworm proposed-update + + -- Mechtilde Stehmann Wed, 18 Sep 2024 16:00:14 +0200 + +mailmindr (1.7.1-1) unstable; urgency=medium + + [ Mechtilde ] + * [c69b29d] New upstream version 1.7.1 + * [c0d7293] Bumped version of dependencies in d/control + + -- Mechtilde Stehmann Sun, 01 Sep 2024 16:26:31 +0200 + +mailmindr (1.6.1-1) unstable; urgency=medium + + [ Mechtilde ] + * [919abc8] New upstream version 1.6.1 + * [1891988] Bumped standard version - no changes needed; + bumpd version of dependency + * [f1ea3a7] Bumped year in d/copyright + + -- Mechtilde Stehmann Wed, 15 May 2024 17:26:47 +0200 + +mailmindr (1.6.0-1) unstable; urgency=medium + + [ Mechtilde ] + * [534d272] New upstream version 1.6.0 + + -- Mechtilde Stehmann Tue, 03 Oct 2023 18:07:14 +0200 + mailmindr (1.4.0-1) unstable; urgency=medium [ Mechtilde ] diff -Nru mailmindr-1.4.0/debian/control mailmindr-1.7.1/debian/control --- mailmindr-1.4.0/debian/control 2022-09-24 18:04:02.000000000 +0000 +++ mailmindr-1.7.1/debian/control 2024-09-01 13:02:05.000000000 +0000 @@ -4,7 +4,7 @@ Maintainer: Debian Mozilla Extension Maintainers Uploaders: Mechtilde Stehmann Build-Depends: debhelper-compat (=13), zip -Standards-Version: 4.6.1 +Standards-Version: 4.7.0 Rules-Requires-Root: no Vcs-Git: https://salsa.debian.org/webext-team/mailmindr.git Vcs-Browser: https://salsa.debian.org/webext-team/mailmindr @@ -13,7 +13,8 @@ Package: webext-mailmindr Architecture: all Depends: ${misc:Depends} - , thunderbird (>=1:102.2) + , thunderbird (>=1:110.10) + , thunderbird (<= 1:129.x) Description: Reminder for emails mailmindr is an addon for the email client "Mozilla Thunderbird". It offers additional functionality to handle the everyday work diff -Nru mailmindr-1.4.0/debian/copyright mailmindr-1.7.1/debian/copyright --- mailmindr-1.4.0/debian/copyright 2022-09-24 18:02:40.000000000 +0000 +++ mailmindr-1.7.1/debian/copyright 2024-05-15 15:16:01.000000000 +0000 @@ -3,11 +3,11 @@ Source: https://mailmindr.net/ Files: * -Copyright: 2013-2022 Arnd Ißler +Copyright: 2013-2024 Arnd Ißler License: MPL-2 Files: debian/* -Copyright: 2019-2022 Mechtilde Stehmann +Copyright: 2019-2024 Mechtilde Stehmann License: MPL-2 License: MPL-2 diff -Nru mailmindr-1.4.0/images/mailmindr-flag--rainbow-shadow.svg mailmindr-1.7.1/images/mailmindr-flag--rainbow-shadow.svg --- mailmindr-1.4.0/images/mailmindr-flag--rainbow-shadow.svg 2022-08-12 13:32:08.000000000 +0000 +++ mailmindr-1.7.1/images/mailmindr-flag--rainbow-shadow.svg 1970-01-01 00:00:00.000000000 +0000 @@ -1,201 +0,0 @@ - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff -Nru mailmindr-1.4.0/images/mailmindr-flag_marker--white.svg mailmindr-1.7.1/images/mailmindr-flag_marker--white.svg --- mailmindr-1.4.0/images/mailmindr-flag_marker--white.svg 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/images/mailmindr-flag_marker--white.svg 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,87 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff -Nru mailmindr-1.4.0/images/mailmindr-flag_marker.svg mailmindr-1.7.1/images/mailmindr-flag_marker.svg --- mailmindr-1.4.0/images/mailmindr-flag_marker.svg 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/images/mailmindr-flag_marker.svg 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,87 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff -Nru mailmindr-1.4.0/manifest.json mailmindr-1.7.1/manifest.json --- mailmindr-1.4.0/manifest.json 2022-08-12 13:32:08.000000000 +0000 +++ mailmindr-1.7.1/manifest.json 2024-08-04 20:14:20.000000000 +0000 @@ -6,13 +6,13 @@ "applications": { "gecko": { "id": "mailmindr@arndissler.net", - "strict_min_version": "78.0", - "strict_max_version": "103.0" + "strict_min_version": "102.0", + "strict_max_version": "129.*" } }, "author": "Arnd Issler", "homepage_url": "https://mailmindr.net/", - "version": "1.4.0", + "version": "1.7.1", "icons": { "16": "images/mailmindr-flag--rainbow.svg", "32": "images/mailmindr-flag--rainbow.svg" @@ -20,15 +20,33 @@ "permissions": [ "activeTab", "accountsRead", + "compose", "menus", "messagesModify", "messagesMove", "messagesRead", + "messagesUpdate", "storage", "tabs", - "tabHide", "unlimitedStorage" ], + "compose_action": { + "browser_style": true, + "default_title": "__MSG_mailmindrComposeMessageButton__", + "default_popup": "views/popups/create-outgoing-mindr/index.html", + "theme_icons": [ + { + "dark": "images/mailmindr-flag.svg", + "light": "images/mailmindr-flag--white.svg", + "size": 16 + }, + { + "dark": "images/mailmindr-flag.svg", + "light": "images/mailmindr-flag--white.svg", + "size": 32 + } + ] + }, "browser_action": { "browser_style": true, "default_title": "__MSG_mailmindrMainToolbarButton__", @@ -77,7 +95,8 @@ "mac": "Command+Shift+1", "chromeos": "Ctrl+Shift+1", "linux": "Ctrl+Shift+1" - } + }, + "description": "__MSG_mailmindrShortcut_SetFollowUp__" }, "mailmindr_open_list": { "suggested_key": { @@ -85,23 +104,8 @@ "mac": "Command+Shift+0", "chromeos": "Ctrl+Shift+0", "linux": "Ctrl+Shift+0" - } - } - }, - "experiment_apis": { - "mailmindrMessagesApi": { - "schema": "schema.json", - "parent": { - "scopes": [ - "addon_parent" - ], - "paths": [ - [ - "mailmindrMessagesApi" - ] - ], - "script": "api/messages.js" - } + }, + "description": "__MSG_mailmindrShortcut_OpenList__" } } } diff -Nru mailmindr-1.4.0/modules/core-utils.mjs.js mailmindr-1.7.1/modules/core-utils.mjs.js --- mailmindr-1.4.0/modules/core-utils.mjs.js 2022-08-12 13:32:08.000000000 +0000 +++ mailmindr-1.7.1/modules/core-utils.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -5,7 +5,42 @@ } from './string-utils.mjs.js'; import { createLogger } from './logger.mjs.js'; -const logger = createLogger('core-utils'); +const logger = createLogger('modules/core-utils'); + +export const throttle = (func, interval) => { + let shouldFire = true; + return () => { + if (shouldFire) { + func(); + shouldFire = false; + setTimeout(() => { + shouldFire = true; + }, interval); + } + }; +}; + +export const sanitizeHeaderMessageId = headerMessageId => + headerMessageId + .replace('>', '') + .replace('<', '') + .trim(); + +export const sendConnectionMessageEx = async ( + openConnections, + message, + connectionName +) => { + logger.info(`open connections?`, openConnections, 'send message', message); + const connection = openConnections.find(con => con.name === connectionName); + if (connection) { + await connection.postMessage(message); + } else { + logger.warn( + `No connection found in ${openConnections.length} open connections` + ); + } +}; /** * Finds the currently running Thunderbird version @@ -33,7 +68,8 @@ export const buildQueryInfoForMessageAndFolder = (messageDetails, folder) => { const { headerMessageId, author, subject: _ } = messageDetails; const queryInfo = { - headerMessageId: headerMessageId.substr(1, headerMessageId.length - 2), + // + headerMessageId: sanitizeHeaderMessageId(headerMessageId), author: getMailAddress(author), folder: { accountId: folder.accountId, @@ -64,7 +100,8 @@ * @returns {boolean} `true` if the mindrs are the same by `headerMessageId` and (internal) `guid` */ export const isSameMindr = (mindrA, mindrB) => - mindrA.headerMessageId === mindrB.headerMessageId && + sanitizeHeaderMessageId(mindrA.headerMessageId) === + sanitizeHeaderMessageId(mindrB.headerMessageId) && mindrA.guid === mindrB.guid; export const createMailmindrId = scope => @@ -80,6 +117,20 @@ export const isExecuted = mindr => mindr.isExecuted; /** + * Returns true when a mindr is waiting for a reply + * @param {Mindr} mindr + * @returns Boolean + */ +export const hasReply = mindr => + mindr.isWaitingForReply && + mindr.metaData && + 'string' === typeof mindr.metaData.replyHeaderMessageId; + +export const showReminderForMindr = mindr => + String(mindr.remindMeMinutesBefore) !== '-1' || + mindr.action.showReminder !== false; + +/** * Checks if a indr is overdue * @param {Mindr} mindr * @returns {boolean} @@ -282,6 +333,20 @@ return actionTemplates; }; +export const createRemindMeMinutesBefore = () => { + const remindMeMinutesBefore = [ + { minutes: 0, display: 0, unit: 'on-time' }, + { minutes: 5, display: 5, unit: 'minutes' }, + { minutes: 15, display: 15, unit: 'minutes' }, + { minutes: 30, display: 30, unit: 'minutes' }, + { minutes: 60, display: 1, unit: 'hours' }, + { minutes: 120, display: 2, unit: 'hours' }, + { minutes: 240, display: 4, unit: 'hours' }, + { minutes: -1, display: null, unit: 'no-reminder' } + ]; + return remindMeMinutesBefore; +}; + /** * Extracts the email address of an email author * @param {string} author The author of an email message, might be an email address or in format Firstname Surname @@ -304,7 +369,9 @@ return author; }; -export const createMindrFromActionTemplate = async mindrData => { +export const createMindrFromActionTemplate = async ( + /** @type {Mindr} */ mindrData +) => { const { guid, headerMessageId, @@ -313,7 +380,8 @@ due, remindMeMinutesBefore, metaData, - isExecuted = false + isExecuted = false, + isWaitingForReply = false } = mindrData; const { flag, @@ -346,7 +414,8 @@ notes, metaData, isExecuted, - modified + modified, + isWaitingForReply }; }; @@ -434,6 +503,10 @@ } const { accountId, name, path, type } = folder; + if (!accountId) { + return null; + } + const account = await browser.accounts.get(accountId); const identityEmailAddressList = account.identities.map( identity => identity.email @@ -465,8 +538,13 @@ } } + if (accountId) { + // + return { accountId, name, path, type }; + } + // - return { accountId, name, path, type }; + return null; }; export const genericFoldersAreEqual = (a, b) => @@ -516,7 +594,7 @@ const { due, remindMeMinutesBefore } = mindr; const dueTime = due.getTime(); const minutesBefore = remindMeMinutesBefore; // Math.max(remindMeMinutesBefore, 0); - console.warn(`* minutes before: ${minutesBefore}`); + // const reminderLookahead = (minutesBefore || lookeahedInMinutes) * 60 * 1000; const remindMeAt = dueTime - reminderLookahead; @@ -563,58 +641,6 @@ }, seed); }; -export const snoozeMindrs = async ( - dispatch, - mindrGuidList, - mindrs, - snoozeTimeMinutes, - correlationId -) => { - do { - const guid = mindrGuidList.pop(); - const theMindr = mindrs.find(mindr => mindr.guid === guid); - - if (theMindr) { - const mindr = { ...theMindr }; - const { due, isExecuted, remindMeMinutesBefore } = mindr; - // - // - - const dueTime = due.getTime(); - const now = Date.now(); - - if (dueTime < now) { - logger.log( - `set snooze (from now): ${mindr.remindMeMinutesBefore}`, - { correlationId, mindrGuid: mindr.guid } - ); - - if (isExecuted) { - mindr.remindMeMinutesBefore = - -1 * (now - dueTime) + (snoozeTimeMinutes || 0); - } else { - // - const diff = Math.floor( - (now + snoozeTimeMinutes * 60 * 1000 - dueTime) / - 60 / - 1000 - ); - mindr.remindMeMinutesBefore = -1 * diff; - } - } - - logger.log(`snoozed by: ${mindr.remindMeMinutesBefore}`, { - correlationId, - mindrGuid: mindr.guid - }); - - await dispatch('mindr:create-or-update', mindr); - } else { - console.log(`ugh: ${theMindr}`); - } - } while (mindrGuidList.length); -}; - /** * * @param {string} identityMailAddress @@ -632,7 +658,9 @@ ); const isIceboxFolderSet = Boolean(iceboxFolderSettings?.folder); - const isDefaultIceboxFolderSet = Boolean(settings?.defaultIceboxFolder); + const isDefaultIceboxFolderSet = Boolean( + settings?.defaultIceboxFolder?.folder + ); logger.info('icebox folder available?', { identityMailAddress, @@ -645,8 +673,62 @@ } if (isDefaultIceboxFolderSet) { - return await localFolderToGenericFolder(settings.defaultIceboxFolder); + return await localFolderToGenericFolder( + settings.defaultIceboxFolder.folder + ); } return null; }; + +/** + * + * @param {Mindr} mindr + * @param { { readonly snoozeTimeMinutes: number; readonly correlationId: string; } } param1 + * @returns {Mindr} + */ +export const snoozeMindr = ( + mindr, + { snoozeTimeMinutes, correlationId = '' } +) => { + const /** @type {EditableMindr} */ modifiedMindr = structuredClone(mindr); + const { due, isExecuted, remindMeMinutesBefore } = modifiedMindr; + + const dueTime = due.getTime(); + const now = Date.now(); + + // + if (dueTime < now) { + const { remindMeMinutesBefore } = modifiedMindr; + + if (isExecuted) { + // + // + // + + if (modifiedMindr.remindMeMinutesBefore < 0) { + modifiedMindr.action.showReminder = false; + } + + modifiedMindr.remindMeMinutesBefore = + // + -1 * + Math.floor( + (now - dueTime + snoozeTimeMinutes * 60 * 1000) / 60 / 1000 + ); + } else { + // + + modifiedMindr.due = new Date( + Date.now() + snoozeTimeMinutes * 60 * 1000 + ); + } + } else { + console.warn(`mindr.remindMeMinutesBefore is not modified`); + modifiedMindr.due = new Date( + Date.now() + remindMeMinutesBefore * 60 * 1000 + ); + } + + return modifiedMindr; +}; diff -Nru mailmindr-1.4.0/modules/defaults.mjs.js mailmindr-1.7.1/modules/defaults.mjs.js --- mailmindr-1.4.0/modules/defaults.mjs.js 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/modules/defaults.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,37 @@ +/** @type {MailmindrState} */ +export const initialState = { + presets: { + actions: [], + time: [] + }, + settings: { + defaultActionPreset: { + copyMessageTo: null, + moveMessageTo: null, + text: null, + tagWithLabel: null, + flag: null, + isSystemAction: false, + markUnread: false, + showReminder: false + }, + defaultIceboxFolder: '', + defaultTimepreset: { + days: 0, + hours: 0, + minutes: 0, + isGenerated: true, + isRelative: false, + isSelectable: false, + text: null + }, + iceboxFolders: [], + snoozeTime: 15 + }, + mindrs: [], + active: [], + overdue: [], + openDialogs: [], + openConnections: [], + __inExecution: [] +}; diff -Nru mailmindr-1.4.0/modules/logger.mjs.js mailmindr-1.7.1/modules/logger.mjs.js --- mailmindr-1.4.0/modules/logger.mjs.js 2022-08-12 13:32:08.000000000 +0000 +++ mailmindr-1.7.1/modules/logger.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -44,9 +44,38 @@ this._connected = false; this._retry = true; this._buffer = []; + this._enabledScopes = []; console.log(`starting logger for scope '${this.scope}'`); - this.tryConnectAndSendBuffer(); + + this.getFilterFromStorage().then(enabledScopes => { + this._enabledScopes = enabledScopes; + let enabled = false; + for (let localScope in enabledScopes) { + let severityString = enabledScopes[localScope]; + let severity = LogLevel[severityString] || LogLevel.WARN; + if (localScope.indexOf('*') >= 0) { + let theScope = localScope.substring( + 0, + localScope.indexOf('*') + ); + if (enabled === false) { + const item = this._scopes.find(s => + s.name.startsWith(theScope) + ); + enabled = item !== undefined; + } + } else { + if (enabled === false) { + // + enabled = this._scopes.find(s => s.name === localScope); + } + } + this._severity = severity; + } + + this.tryConnectAndSendBuffer(); + }); } createContextLogger(scope) { @@ -65,6 +94,18 @@ } } + async getFilterFromStorage() { + try { + const { logFilter } = await messenger.storage.local.get( + 'logFilter' + ); + + return logFilter; + } catch (exception) { + return []; + } + } + async tryConnectAndSendBuffer() { let count = 0; while (this._connected === false && this._retry) { @@ -115,6 +156,10 @@ context }; + if (this._severity > severity) { + return; + } + this.trySend(logItem); switch (severity) { diff -Nru mailmindr-1.4.0/modules/message-actions.mjs.js mailmindr-1.7.1/modules/message-actions.mjs.js --- mailmindr-1.4.0/modules/message-actions.mjs.js 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/modules/message-actions.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,187 @@ +import { createCorrelationId, createLogger } from '../modules/logger.mjs.js'; +import { + genericFolderToLocalFolder, + getFlatFolderList +} from '../modules/core-utils.mjs.js'; +import { + applyActionToMessageInFolder, + doMoveMessageToFolder +} from './message-utils.mjs.js'; + +const logger = createLogger('modules/message-actions'); + +export const executeMindr = async mindr => { + const { headerMessageId, metaData, action, guid } = mindr; + logger.log(`START executeMindr ${guid} w/ msgHdrId: '${headerMessageId}'`, { + guid, + headerMessageId + }); + const { + author, + subject, + folderAccountId, + folderName, + folderPath, + folderType, + folderAccountIdentityMailAddress + } = metaData; + const correlationId = createCorrelationId('executeMindr'); + const executionStart = Date.now(); + const { copyMessageTo, moveMessageTo } = action; + + const applyAction = async (messageId, action) => { + const { + flag, + markUnread, + showReminder, + tagWithLabel, + copyMessageTo, + moveMessageTo + } = action; + const messageProps = { + ...(flag && { flagged: true }), + ...(markUnread && { read: false }) + }; + + logger.info(`→ apply update to message ${messageId}`, messageProps); + + await messenger.messages.update(messageId, messageProps); + }; + + const destinationFolder = moveMessageTo + ? await genericFolderToLocalFolder(moveMessageTo) + : null; + const possibleSourceFolder = await genericFolderToLocalFolder({ + accountId: folderAccountId, + path: folderPath, + identityEmailAddress: folderAccountIdentityMailAddress + }); + const flatFolderList = await getFlatFolderList(); + const localFlatFolderList = await Promise.all( + flatFolderList + .filter(({ type }) => type === 'folder') + .map(async ({ folder }) => await genericFolderToLocalFolder(folder)) + ); + const folders = [ + possibleSourceFolder, + ...localFlatFolderList.filter( + fldr => + fldr.accountId !== possibleSourceFolder.accountId && + fldr.path !== possibleSourceFolder.path + ) + ]; + + logger.log(`BEGIN execution of ${mindr.guid}`, { + correlationId, + guid + }); + const startTime = performance.now(); + + const targetFolders = folders; + + let hasError = false; + let iterationCount = 0; + + logger.log(`BEGIN targetFolder iteration`, { + guid, + correlationId, + targetFolderCount: (targetFolders || []).length, + targetFolders + }); + + const applyActionToMessage = async (message, messageFolder) => { + const { id } = message; + + logger.log(`BEGIN applyActionToMessage`); + await applyAction(id, action); + logger.log( + `Do we have a destination folder? ${ + destinationFolder ? 'yes' : 'no' + }`, + destinationFolder + ); + if (destinationFolder) { + await doMoveMessageToFolder( + message, + destinationFolder, + correlationId + ); + } + logger.log(`END applyActionToMessage`); + }; + + const applyActionToFirstMessageInFolders = async () => { + for await (let folder of targetFolders) { + logger.log(` -- executeMindr: folder loop (${folder.name})`, { + correlationId, + folder + }); + + try { + const actionResult = await applyActionToMessageInFolder( + folder, + { headerMessageId, author }, + applyActionToMessage, + true + ); + const { done, value } = await actionResult.next(); + const success = Boolean(done && value && value.executed); + if (success) { + return true; + } + } catch (ex) { + logger.error('ERROR: execute mindr // mailmindr: >> !!', { + correlationId, + guid, + exception: ex + }); + hasError = true; + } + iterationCount++; + } + return false; + }; + + await applyActionToFirstMessageInFolders(); + + logger.log(`END targetFolder iteration`, { + correlationId, + guid, + targetFolderCount: (targetFolders || []).length, + targetFolders + }); + + const endTime = performance.now(); + logger.log( + `mailmindr: execution finished in ${endTime - startTime}ms`, + moveMessageTo + ); + + const executionEnd = Date.now(); + const executionDuration = (executionEnd - executionStart) / 1000; + + if (executionDuration > 3 * 60) { + logger.error( + `Execution of mindr '${guid}' took more than 180 seconds`, + { guid, correlationId, executionDuration } + ); + } else if (executionDuration > 60) { + logger.warn(`Execution of mindr '${guid}' took more than 60 seconds`, { + guid, + correlationId, + executionDuration + }); + } else { + logger.warn( + `Execution of mindr '${guid}' took ${executionDuration} seconds`, + { guid, correlationId, executionDuration } + ); + } + logger.log(`END execution of ${mindr.guid}`, { correlationId, guid }); + logger.log(`END executeMindr ${guid} w/ msgHdrId: '${headerMessageId}'`, { + guid, + headerMessageId + }); + + return !hasError; +}; diff -Nru mailmindr-1.4.0/modules/message-utils.mjs.js mailmindr-1.7.1/modules/message-utils.mjs.js --- mailmindr-1.4.0/modules/message-utils.mjs.js 2022-08-12 13:32:08.000000000 +0000 +++ mailmindr-1.7.1/modules/message-utils.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -1,10 +1,27 @@ import { buildQueryInfoForMessageAndFolder, - getMailAddress + getMailAddress, + sanitizeHeaderMessageId } from './core-utils.mjs.js'; import { createLogger, createCorrelationId } from './logger.mjs.js'; -const logger = createLogger('modules.message-utils'); +const logger = createLogger('modules/message-utils'); + +export const getMessages = async function*( + /** @type {messenger.messages.MessageList} */ list +) { + let page = list; + for (let message of page.messages) { + yield message; + } + + while (page.id) { + page = await messenger.messages.continueList(page.id); + for (let message of page.messages) { + yield message; + } + } +}; const findMessage = async ( messages, @@ -55,9 +72,11 @@ } const msgWithHeader = await browser.messages.getFull(id); - const msgHdrId0 = msgWithHeader.headers['message-id'][0]; + const msgHdrId0 = sanitizeHeaderMessageId( + msgWithHeader.headers['message-id'][0] + ); - if (msgHdrId0 === headerMessageId) { + if (msgHdrId0 === sanitizeHeaderMessageId(headerMessageId)) { logger.log( `SUCCESS headerMessageId found: '${headerMessageId}' === '${msgHdrId0}'` ); @@ -106,7 +125,21 @@ queryInfo }); - let queryResult = await messenger.messages.query(queryInfo); + let queryResult = null; + try { + queryResult = await messenger.messages.query(queryInfo); + } catch (queryError) { + // + queryResult = null; + logger.error(`applyActionToMessageInFolder: initial query failed`, { + queryError, + queryInfo + }); + + throw queryError; + + return Promise.resolve(null); + } logger.log(`messages query done, result is`, { correlationId, @@ -159,3 +192,95 @@ return Promise.resolve(null); } + +export const doMoveMessageToFolder = async ( + message, + destinationFolder, + parentCorrelationId +) => { + const correlationId = createCorrelationId( + 'doMoveMessageToFolder', + parentCorrelationId + ); + try { + const { id } = message; + const { accountId, path } = destinationFolder; + logger.log(`BEGIN move message ${id}`, { + correlationId, + message + }); + await messenger.messages.move([id], { accountId, path }); + logger.log(`END move message ${id}`, { + correlationId, + message + }); + return true; + } catch (error) { + logger.error(error, { correlationId }); + return false; + } +}; + +export const moveMessageToFolder = async ( + messageDetails, + sourceFolder, + targetFolder +) => { + const correlationId = createCorrelationId('moveMessagesToFolder'); + try { + logger.info('moveMessageToFolder target', { + correlationId, + targetFolder + }); + + const moveMessageToFolderAction = async ( + message, + _messageSourceFolder + ) => { + await doMoveMessageToFolder(message, targetFolder, correlationId); + }; + + logger.info(`BEGIN applying action to folder`, { + correlationId, + sourceFolder, + messageDetails + }); + const actionResult = await applyActionToMessageInFolder( + sourceFolder, + messageDetails, + moveMessageToFolderAction, + true + ); + const { done, value } = await actionResult.next(); + + // + const success = Boolean(done); + + const logMessage = `moveMessageToFolder : applyActionToMessageInFolder returns { done: ${done}, value: ${value} }`; + if (success) { + logger.info(logMessage, { correlationId, result: { done, value } }); + } else { + logger.warn(logMessage, { correlationId, result: { done, value } }); + } + + if (value === null) { + logger.error( + `moveMessageToFolder : applyActionToMessageInFolder message not found in folder` + ); + } + + logger.info(`END applying action to folder`, { + correlationId, + sourceFolder, + messageDetails + }); + } catch (e) { + logger.error(`moveMessageToFolder failed: ${e.message}`, { + correlationId, + error: e + }); + return false; + } + + return true; +}; diff -Nru mailmindr-1.4.0/modules/storage.mjs.js mailmindr-1.7.1/modules/storage.mjs.js --- mailmindr-1.4.0/modules/storage.mjs.js 2022-08-12 13:32:08.000000000 +0000 +++ mailmindr-1.7.1/modules/storage.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -1,6 +1,6 @@ import { createCorrelationId, createLogger } from './logger.mjs.js'; -const logger = createLogger('background'); +const logger = createLogger('modules/storage'); const tryParseOrReturnDefault = (content, defaultValue) => { if (typeof content === 'object' && content !== null) { diff -Nru mailmindr-1.4.0/modules/store/actions/actionTypes.mjs.js mailmindr-1.7.1/modules/store/actions/actionTypes.mjs.js --- mailmindr-1.4.0/modules/store/actions/actionTypes.mjs.js 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/modules/store/actions/actionTypes.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,16 @@ +export const ACTION__HEARTBEAT = 'heartbeat'; +export const ACTION__LOCK_MINDR_FOR_EXECUTION = 'state:lock-execution'; +export const ACTION__UNLOCK_MINDR = 'state:unlock-execution'; +export const ACTION__MINDR_CREATE_OR_UPDATE_DRAFT = + 'mindr:create-or-update-draft'; +export const ACTION__MINDR_CREATE_OR_UPDATE = 'mindr:create-or-update'; +export const ACTION__MINDR_REMOVE = 'mindr:remove'; +export const ACTION__MINDR_REMOVE_DRAFT = 'mindr:remove-draft'; +export const ACTION__SETTINGS_UPDATE = 'setting:update'; +export const ACTION__DIALOG_OPEN = 'dialog:open'; +export const ACTION__DIALOG_CLOSE = 'dialog:close'; +export const ACTION__CONNECTION_OPEN = 'connection:open'; +export const ACTION__CONNECTION_CLOSE = 'connection:close'; +export const ACTION__PRESET_TIMESPAN_CREATE = 'preset:timespan-create'; +export const ACTION__PRESET_TIMESPAN_UPDATE = 'preset:timespan-update'; +export const ACTION__PRESET_TIMESPAN_REMOVE = 'preset:timespan-remove'; diff -Nru mailmindr-1.4.0/modules/store/actions/actions.mjs.js mailmindr-1.7.1/modules/store/actions/actions.mjs.js --- mailmindr-1.4.0/modules/store/actions/actions.mjs.js 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/modules/store/actions/actions.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,91 @@ +import { + ACTION__CONNECTION_CLOSE, + ACTION__CONNECTION_OPEN, + ACTION__DIALOG_CLOSE, + ACTION__DIALOG_OPEN, + ACTION__HEARTBEAT, + ACTION__LOCK_MINDR_FOR_EXECUTION, + ACTION__MINDR_CREATE_OR_UPDATE, + ACTION__MINDR_CREATE_OR_UPDATE_DRAFT, + ACTION__MINDR_REMOVE, + ACTION__MINDR_REMOVE_DRAFT, + ACTION__PRESET_TIMESPAN_CREATE, + ACTION__PRESET_TIMESPAN_REMOVE, + ACTION__PRESET_TIMESPAN_UPDATE, + ACTION__SETTINGS_UPDATE, + ACTION__UNLOCK_MINDR +} from './actionTypes.mjs.js'; + +export const heartBeat = () => ({ + type: ACTION__HEARTBEAT +}); + +export const lockMindrForExecution = mindr => ({ + type: ACTION__LOCK_MINDR_FOR_EXECUTION, + payload: { guid: mindr.guid } +}); + +export const unlockMindr = mindr => ({ + type: ACTION__UNLOCK_MINDR, + payload: { guid: mindr.guid } +}); + +export const createOrUpdateDraft = draft => ({ + type: ACTION__MINDR_CREATE_OR_UPDATE_DRAFT, + payload: draft +}); + +export const createOrUpdateMindr = mindr => ({ + type: ACTION__MINDR_CREATE_OR_UPDATE, + payload: { mindr } +}); + +export const openDialog = (dialogId, dialogType, details) => ({ + type: ACTION__DIALOG_OPEN, + payload: { dialogId, dialogType, details } +}); + +export const closeDialog = dialogId => ({ + type: ACTION__DIALOG_CLOSE, + payload: { dialogId } +}); + +export const removeMindr = guid => ({ + type: ACTION__MINDR_REMOVE, + payload: { guid } +}); + +export const removeDraft = (/** @type {MindrDraft} */ draft) => ({ + type: ACTION__MINDR_REMOVE_DRAFT, + payload: { draft } +}); + +export const connectionOpened = port => ({ + type: ACTION__CONNECTION_OPEN, + payload: { port } +}); + +export const connectionClosed = port => ({ + type: ACTION__CONNECTION_CLOSE, + payload: { port } +}); + +export const createTimespanPreset = current => ({ + type: ACTION__PRESET_TIMESPAN_CREATE, + payload: { current } +}); + +export const updateTimespanPreset = (current, source) => ({ + type: ACTION__PRESET_TIMESPAN_UPDATE, + payload: { current, source } +}); + +export const removeTimespanPreset = presets => ({ + type: ACTION__PRESET_TIMESPAN_REMOVE, + payload: { presets } +}); + +export const updateSetting = (name, value) => ({ + type: ACTION__SETTINGS_UPDATE, + payload: { name, value } +}); diff -Nru mailmindr-1.4.0/modules/store/actions/executeMindr.mjs.js mailmindr-1.7.1/modules/store/actions/executeMindr.mjs.js --- mailmindr-1.4.0/modules/store/actions/executeMindr.mjs.js 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/modules/store/actions/executeMindr.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,215 @@ +import { + genericFolderToLocalFolder, + getFlatFolderList +} from '../../core-utils.mjs.js'; +import { createCorrelationId, createLogger } from '../../logger.mjs.js'; +import { + applyActionToMessageInFolder, + doMoveMessageToFolder +} from '../../message-utils.mjs.js'; +import { + lockMindrForExecution, + unlockMindr, + createOrUpdateMindr +} from './actions.mjs.js'; + +const logger = createLogger('modules/store/actions/executeMindr'); + +export const executeMindr = mindr => async (dispatch, getState) => { + const { headerMessageId, metaData, action, guid } = mindr; + logger.log(`START executeMindr ${guid} w/ msgHdrId: '${headerMessageId}'`, { + guid, + headerMessageId + }); + + dispatch(lockMindrForExecution(mindr)); + + const { + author, + subject, + folderAccountId, + folderName, + folderPath, + folderType, + folderAccountIdentityMailAddress + } = metaData; + const correlationId = createCorrelationId('executeMindr'); + const executionStart = Date.now(); + const { copyMessageTo, moveMessageTo } = action; + + const applyAction = async (messageId, action) => { + const { + flag, + markUnread, + showReminder, + tagWithLabel, + copyMessageTo, + moveMessageTo + } = action; + const messageProps = { + ...(flag && { flagged: true }), + ...(markUnread && { read: false }) + }; + + logger.info(`→ apply update to message ${messageId}`, messageProps); + + const timeout = new Promise(resolve => setTimeout(resolve, 1000)); + const updater = messenger.messages.update(messageId, messageProps); + + await Promise.all([updater, timeout]); + }; + + const destinationFolder = moveMessageTo + ? await genericFolderToLocalFolder(moveMessageTo) + : null; + const possibleSourceFolder = await genericFolderToLocalFolder({ + accountId: folderAccountId, + path: folderPath, + identityEmailAddress: folderAccountIdentityMailAddress + }); + const flatFolderList = await getFlatFolderList(); + const localFlatFolderList = await Promise.all( + flatFolderList + .filter(({ type }) => type === 'folder') + .map(async ({ folder }) => await genericFolderToLocalFolder(folder)) + ); + const folders = possibleSourceFolder + ? [ + possibleSourceFolder, + ...localFlatFolderList.filter( + fldr => + fldr.accountId !== possibleSourceFolder.accountId && + fldr.path !== possibleSourceFolder.path + ) + ] + : localFlatFolderList; + + logger.log(`BEGIN execution of ${mindr.guid}`, { + correlationId, + guid + }); + const startTime = performance.now(); + + const targetFolders = folders; + + let hasError = false; + let iterationCount = 0; + + logger.log(`BEGIN targetFolder iteration`, { + guid, + correlationId, + targetFolderCount: (targetFolders || []).length, + targetFolders + }); + + const applyActionToMessage = async (message, messageFolder) => { + const { id } = message; + + logger.log(`BEGIN applyActionToMessage`); + await applyAction(id, action); + logger.log( + `Do we have a destination folder? ${ + destinationFolder ? 'yes' : 'no' + }`, + destinationFolder + ); + if (destinationFolder) { + await doMoveMessageToFolder( + message, + destinationFolder, + correlationId + ); + } + logger.log(`END applyActionToMessage`); + }; + + const applyActionToFirstMessageInFolders = async () => { + for await (let folder of targetFolders) { + logger.log( + ` -- executeMindr: folder loop, apply action to message in folder '(${folder.name})'`, + { + correlationId, + folder, + targetFolders + } + ); + + try { + const actionResult = await applyActionToMessageInFolder( + folder, + { headerMessageId, author }, + applyActionToMessage, + true + ); + const { done, value } = await actionResult.next(); + const success = Boolean(done && value && value.executed); + if (success) { + return true; + } + } catch (ex) { + logger.error('ERROR: execute mindr // mailmindr: >> !!', { + correlationId, + guid, + exception: ex + }); + hasError = true; + } + iterationCount++; + } + return false; + }; + + logger.log(`BEFORE applying actions`, { correlationId }); + await applyActionToFirstMessageInFolders(); + logger.log(`END applying actions`, { correlationId }); + + logger.log(`END targetFolder iteration`, { + correlationId, + guid, + targetFolderCount: (targetFolders || []).length, + targetFolders + }); + + const endTime = performance.now(); + logger.log( + `mailmindr: execution finished in ${endTime - startTime}ms`, + moveMessageTo + ); + + const executionEnd = Date.now(); + const executionDuration = (executionEnd - executionStart) / 1000; + + if (executionDuration > 3 * 60) { + logger.error( + `Execution of mindr '${guid}' took more than 180 seconds`, + { guid, correlationId, executionDuration } + ); + } else if (executionDuration > 60) { + logger.warn(`Execution of mindr '${guid}' took more than 60 seconds`, { + guid, + correlationId, + executionDuration + }); + } else { + logger.warn( + `Execution of mindr '${guid}' took ${executionDuration} seconds`, + { guid, correlationId, executionDuration } + ); + } + logger.log(`END execution of ${mindr.guid}`, { correlationId, guid }); + logger.log(`END executeMindr ${guid} w/ msgHdrId: '${headerMessageId}'`, { + guid, + headerMessageId + }); + + const modifiedMindr = structuredClone(mindr); + modifiedMindr.isExecuted = true; + + dispatch(unlockMindr(modifiedMindr)); + + if (!hasError) { + dispatch(createOrUpdateMindr(modifiedMindr)); + } + + return !hasError; +}; diff -Nru mailmindr-1.4.0/modules/store/actions/heartBeat.mjs.js mailmindr-1.7.1/modules/store/actions/heartBeat.mjs.js --- mailmindr-1.4.0/modules/store/actions/heartBeat.mjs.js 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/modules/store/actions/heartBeat.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,49 @@ +import { isExecuted, showReminderForMindr } from '../../core-utils.mjs.js'; +import { createLogger } from '../../logger.mjs.js'; +import { executeMindr } from './executeMindr.mjs.js'; +import { showMindrAlert } from './index.mjs.js'; + +const logger = createLogger('modules/store/actions/heartBeat'); + +export const heartBeatEx = () => async (dispatch, getState) => { + const { overdue, active, __inExecution } = getState(); + const overdueNotExecuted = overdue.filter(item => !item.isExecuted); + + logger.log(`heartBeatEx -- `, { overdue, active, overdueNotExecuted }); + + if (Array.isArray(__inExecution) && __inExecution.length > 0) { + logger.warn( + `Mindr is executing (${__inExecution.length} in total), skipping further executions`, + { overdue, active, __inExecution } + ); + return; + } + + logger.log(`overdueNotExecuted: `, overdueNotExecuted); + for (let mindr of overdueNotExecuted) { + dispatch(executeMindr(mindr)); + } + + // + // + const overdueAndUnexecuted = overdue.filter( + mindr => !isExecuted(mindr) && showReminderForMindr(mindr) + ); + + const activeMindrs = active.filter(showReminderForMindr); + + if (overdueAndUnexecuted.length > 0 || activeMindrs.length > 0) { + logger.info(`heartbeat: show dialog with mindrs `, { + overdueAndUnexecuted, + active: activeMindrs + }); + dispatch( + showMindrAlert({ + overdue: overdueAndUnexecuted, + active: activeMindrs + }) + ); + } else { + logger.log('No reason to show a dialog', { overdueAndUnexecuted }); + } +}; diff -Nru mailmindr-1.4.0/modules/store/actions/index.mjs.js mailmindr-1.7.1/modules/store/actions/index.mjs.js --- mailmindr-1.4.0/modules/store/actions/index.mjs.js 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/modules/store/actions/index.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,7 @@ +export { executeMindr } from './executeMindr.mjs.js'; +export { snoozeMindrs } from './snoozeMindrs.mjs.js'; +export { heartBeatEx } from './heartBeat.mjs.js'; +export { showMindrAlert } from './showMindrAlert.mjs.js'; +export { setReplyReceived } from './setReplyReceived.mjs.js'; + +export * from './actions.mjs.js'; diff -Nru mailmindr-1.4.0/modules/store/actions/setReplyReceived.mjs.js mailmindr-1.7.1/modules/store/actions/setReplyReceived.mjs.js --- mailmindr-1.4.0/modules/store/actions/setReplyReceived.mjs.js 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/modules/store/actions/setReplyReceived.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,24 @@ +import { createLogger } from '../../logger.mjs.js'; +import { createOrUpdateMindr } from './index.mjs.js'; + +const logger = createLogger('actions/setReplyReceived'); + +export const setReplyReceived = ( + /** @type {Mindr} */ theMindr, + /** @type {string} */ replyHeaderMessageId +) => async (dispatch, _getState) => { + if (theMindr) { + const mindr = structuredClone(theMindr); // { ...theMindr }; + const modifiedMindr = { + ...mindr, + /** @type {Mindr['metaData']} */ metaData: { + ...mindr.metaData, + replyHeaderMessageId + } + }; + + await dispatch(createOrUpdateMindr(modifiedMindr)); + } else { + logger.error(`mindr is not defined: ${theMindr}`); + } +}; diff -Nru mailmindr-1.4.0/modules/store/actions/showMindrAlert.mjs.js mailmindr-1.7.1/modules/store/actions/showMindrAlert.mjs.js --- mailmindr-1.4.0/modules/store/actions/showMindrAlert.mjs.js 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/modules/store/actions/showMindrAlert.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,105 @@ +import { + createMailmindrId, + sendConnectionMessageEx +} from '../../core-utils.mjs.js'; +import { createLogger } from '../../logger.mjs.js'; +import { + selectDialogForType, + selectOpenConnections +} from '../selectors/index.mjs.js'; +import { closeDialog, openDialog } from './actions.mjs.js'; + +const logger = createLogger('modules/store/actions/showMindrAlert'); + +export const showMindrAlert = ({ overdue, active }) => async ( + dispatch, + getState +) => { + const dialogType = 'mailmindr:mindr-alert'; + const state = getState(); + const openConnections = selectOpenConnections(state); + + const dialogs = (await messenger.tabs.query({})).filter(tab => { + if (tab && tab.url) { + return tab.url.indexOf('/mindr-alert/') > 0; + } + return false; + }); + const alertDialog = dialogs && dialogs.length && dialogs[0]; + const dialog = alertDialog + ? { details: { id: alertDialog.windowId, tabId: alertDialog.id } } + : null; + + if (dialog) { + logger.log('we already have a message dialog', dialog); + + if ((overdue || []).length === 0 && (active || []).length === 0) { + logger.log(`no overdue or active mindrs → we can close the dialog`); + + // + dispatch(closeDialog(dialog.details.id)); + messenger.windows.remove(dialog.details.id); + } else { + logger.log(`we have a dialog and send data to it`, { + overdue, + active + }); + + // + await sendConnectionMessageEx( + openConnections, + { + overdue, + active + }, + 'connection:mindr-alert' + ); + await messenger.windows.update(dialog.details.id, { + focused: false, + drawAttention: true + }); + } + } else { + logger.log( + 'need to open a new dialog with active/overdue mindrs', + active, + overdue + ); + if ((active || []).length === 0 && (overdue || []).length === 0) { + logger.log('no dialog needed'); + return; + } + const dialogId = createMailmindrId('mailmindr:dialog:mindr-alert'); + + const parameters = new URLSearchParams(); + parameters.set('dialogId', dialogId); + + const { width: screenWidth, availHeight: screenHeight } = screen; + const height = 200; + const width = 400; + const left = screenWidth - width; + const top = screenHeight - height; + const url = `/views/dialogs/mindr-alert/index.html?${parameters}`; + const details = await messenger.windows.create({ + left, + top, + height, + width, + url, + type: 'popup', + state: 'normal', + allowScriptsToClose: true + }); + + await messenger.windows.update(details.id, { + top, + left, + width, + height, + focused: true, + drawAttention: true + }); + + dispatch(openDialog(dialogId, dialogType, details)); + } +}; diff -Nru mailmindr-1.4.0/modules/store/actions/snoozeMindrs.mjs.js mailmindr-1.7.1/modules/store/actions/snoozeMindrs.mjs.js --- mailmindr-1.4.0/modules/store/actions/snoozeMindrs.mjs.js 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/modules/store/actions/snoozeMindrs.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,29 @@ +import { snoozeMindr } from '../../core-utils.mjs.js'; +import { createLogger } from '../../logger.mjs.js'; +import { createOrUpdateMindr } from './index.mjs.js'; + +const logger = createLogger('modules/store/actions/snoozeMindrs'); + +export const snoozeMindrs = ( + mindrGuidList, + mindrs, + snoozeTimeMinutes, + correlationId +) => async (dispatch, getState) => { + do { + const guid = mindrGuidList.pop(); + const theMindr = mindrs.find(mindr => mindr.guid === guid); + + if (theMindr) { + const mindr = structuredClone(theMindr); // { ...theMindr }; + const modifiedMindr = snoozeMindr(mindr, { + snoozeTimeMinutes, + correlationId + }); + + await dispatch(createOrUpdateMindr(modifiedMindr)); + } else { + logger.log(`ugh: ${theMindr}`); + } + } while (mindrGuidList.length); +}; diff -Nru mailmindr-1.4.0/modules/store/reducers/createOrUpdateDraft.mjs.js mailmindr-1.7.1/modules/store/reducers/createOrUpdateDraft.mjs.js --- mailmindr-1.4.0/modules/store/reducers/createOrUpdateDraft.mjs.js 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/modules/store/reducers/createOrUpdateDraft.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,33 @@ +import { createLogger } from '../../logger.mjs.js'; +import { + ACTION__MINDR_CREATE_OR_UPDATE, + ACTION__MINDR_CREATE_OR_UPDATE_DRAFT +} from '../actions/actionTypes.mjs.js'; + +const logger = createLogger('reducers/createOrUpdateDraft'); + +export const createOrUpdateDraftReducer = ( + /** @type {MailmindrState} */ state, + action +) => { + const { type, payload } = action; + if (type !== ACTION__MINDR_CREATE_OR_UPDATE_DRAFT) { + return state; + } + + const { mindr, sender } = payload; + const { __drafts: drafts, ...stateWithoutMindrs } = state; + const mindrsWithoutUpdatedMindr = (drafts || []).filter( + item => + item.sender.id !== sender.id && + item.sender.windowId !== sender.windowId + ); + const updatedDrafts = [...mindrsWithoutUpdatedMindr, payload]; + + const localState = { + ...stateWithoutMindrs, + __drafts: updatedDrafts + }; + + return localState; +}; diff -Nru mailmindr-1.4.0/modules/store/reducers/createOrUpdateMindr.mjs.js mailmindr-1.7.1/modules/store/reducers/createOrUpdateMindr.mjs.js --- mailmindr-1.4.0/modules/store/reducers/createOrUpdateMindr.mjs.js 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/modules/store/reducers/createOrUpdateMindr.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,26 @@ +import { isSameMindr } from '../../core-utils.mjs.js'; +import { createLogger } from '../../logger.mjs.js'; +import { ACTION__MINDR_CREATE_OR_UPDATE } from '../actions/actionTypes.mjs.js'; + +const logger = createLogger('modules/store/reducers/createOrUpdateMindr'); + +export const createOrUpdateMindrReducer = (state, action) => { + const { type, payload } = action; + if (type !== ACTION__MINDR_CREATE_OR_UPDATE) { + return state; + } + + const { mindr } = payload; + const { mindrs: allMindrs, ...stateWithoutMindrs } = state; + const mindrsWithoutUpdatedMindr = allMindrs.filter( + item => !isSameMindr(item, mindr) + ); + const mindrs = [...mindrsWithoutUpdatedMindr, mindr]; + + const localState = { + ...stateWithoutMindrs, + mindrs + }; + + return localState; +}; diff -Nru mailmindr-1.4.0/modules/store/reducers/index.mjs.js mailmindr-1.7.1/modules/store/reducers/index.mjs.js --- mailmindr-1.4.0/modules/store/reducers/index.mjs.js 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/modules/store/reducers/index.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,216 @@ +import { + equalTimePresetValues, + getActiveAndOverdueMindrs +} from '../../core-utils.mjs.js'; +import { createLogger } from '../../logger.mjs.js'; +import { + ACTION__CONNECTION_CLOSE, + ACTION__CONNECTION_OPEN, + ACTION__DIALOG_CLOSE, + ACTION__DIALOG_OPEN, + ACTION__HEARTBEAT, + ACTION__LOCK_MINDR_FOR_EXECUTION, + ACTION__MINDR_CREATE_OR_UPDATE, + ACTION__MINDR_CREATE_OR_UPDATE_DRAFT, + ACTION__MINDR_REMOVE, + ACTION__MINDR_REMOVE_DRAFT, + ACTION__PRESET_TIMESPAN_CREATE, + ACTION__PRESET_TIMESPAN_REMOVE, + ACTION__PRESET_TIMESPAN_UPDATE, + ACTION__SETTINGS_UPDATE, + ACTION__UNLOCK_MINDR +} from '../actions/actionTypes.mjs.js'; +import { selectOpenConnections } from '../selectors/index.mjs.js'; +import { createOrUpdateDraftReducer } from './createOrUpdateDraft.mjs.js'; +import { createOrUpdateMindrReducer } from './createOrUpdateMindr.mjs.js'; +import { removeMindrReducer } from './removeMindr.mjs.js'; + +const logger = createLogger('modules/store/reducers/root'); + +const rootReducer = (/** @type {MailmindrState} */ state, action) => { + const { type, payload } = action; + switch (type) { + case ACTION__HEARTBEAT: + const { mindrs: mindrList } = state; + + const { mindrs, overdue, active } = getActiveAndOverdueMindrs( + mindrList + ); + + // + // + // + + return { ...state, mindrs, active, overdue }; + case ACTION__CONNECTION_OPEN: { + const { port } = payload; + const openConnections = [...selectOpenConnections(state), port]; + + logger.log( + `open connections: ${openConnections.length}`, + openConnections + ); + + return { ...state, openConnections }; + } + case ACTION__CONNECTION_CLOSE: { + const { port } = payload; + const { name } = port; + const { openConnections: connections } = state; + + const openConnections = connections.filter( + connection => connection.name !== name + ); + + return { ...state, openConnections }; + } + case ACTION__MINDR_CREATE_OR_UPDATE_DRAFT: + return createOrUpdateDraftReducer(state, action); + case ACTION__MINDR_CREATE_OR_UPDATE: + return createOrUpdateMindrReducer(state, action); + case ACTION__MINDR_REMOVE: + return removeMindrReducer(state, action); + case ACTION__MINDR_REMOVE_DRAFT: { + const { /** @type {MindrDraft}*/ draft } = payload; + const localState = { + ...state, + __drafts: state.__drafts.filter( + item => + item.sender.id !== draft.sender.id && + item.sender.windowId !== draft.sender.windowId + ) + }; + return localState; + } + case ACTION__LOCK_MINDR_FOR_EXECUTION: { + // + // + const { guid } = payload; + + const localState = { + ...state, + __inExecution: [...state.__inExecution, guid] + }; + + return localState; + } + case ACTION__UNLOCK_MINDR: { + // + const { guid } = payload; + + const localState = { + ...state, + __inExecution: state.__inExecution.filter(item => item !== guid) + }; + + return localState; + } + case ACTION__SETTINGS_UPDATE: { + const { name, value } = payload; + + const localState = { + ...state, + settings: { ...state.settings, [name]: value } + }; + + return localState; + } + case ACTION__PRESET_TIMESPAN_CREATE: { + const { presets } = state; + const { time } = presets; + const { current } = payload; + + const localState = { + ...state, + presets: { + ...presets, + time: [...time, current] + } + }; + + return localState; + } + case ACTION__PRESET_TIMESPAN_UPDATE: { + const { presets } = state; + const { time: timePresets } = presets; + const { current, source } = payload; + + const time = timePresets.map(item => + equalTimePresetValues(item, source) ? current : item + ); + + logger.info(`new presets:`, time); + + const newState = { + ...state, + presets: { + ...presets, + time + } + }; + + return newState; + } + case ACTION__PRESET_TIMESPAN_REMOVE: { + const { presets } = state; + const { time: timePresets } = presets; + const { presets: toBeRemoved = [] } = payload; + + let time = [...timePresets]; + toBeRemoved.forEach(toBeRemovedPreset => { + time = time.filter( + preset => !equalTimePresetValues(preset, toBeRemovedPreset) + ); + }); + + const newState = { + ...state, + presets: { + ...presets, + time + } + }; + + return newState; + } + case ACTION__DIALOG_OPEN: { + const newDialogDetails = payload; + const openDialogs = [...state.openDialogs, newDialogDetails]; + + return { ...state, openDialogs }; + } + case ACTION__DIALOG_CLOSE: { + const { dialogId } = payload; + const { openDialogs: dialogs } = state; + const dialogDetails = dialogs.find( + dialogInfo => dialogId === dialogInfo.dialogId + ); + + if (dialogDetails) { + const openDialogs = dialogs.filter( + openDialog => openDialog.dialogId !== dialogId + ); + logger.info(`remaining open dialogs: ${openDialogs.length}`); + return { + ...state, + openDialogs + }; + } else { + logger.warn( + `cannot find details for open dialog ID: '${dialogId}'`, + { + payload, + dialogId, + openDialogs: dialogs + } + ); + } + + return state; + } + default: + return state; + } +}; + +export default rootReducer; diff -Nru mailmindr-1.4.0/modules/store/reducers/removeMindr.mjs.js mailmindr-1.7.1/modules/store/reducers/removeMindr.mjs.js --- mailmindr-1.4.0/modules/store/reducers/removeMindr.mjs.js 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/modules/store/reducers/removeMindr.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,52 @@ +import { createLogger } from '../../logger.mjs.js'; +import { ACTION__MINDR_REMOVE } from '../actions/actionTypes.mjs.js'; + +const logger = createLogger('modules/store/reducers/removeMindr'); + +export const removeMindrReducer = (state, action) => { + const { type, payload } = action; + if (type !== ACTION__MINDR_REMOVE) { + return state; + } + + const { guid } = payload; + const { + mindrs: stateMindrs, + overdue: stateOverdue, + active: stateActive + } = state; + + const mindrCount = { + mindrs: (stateMindrs || []).length, + overdue: (stateOverdue || []).length, + active: (stateActive || []).length + }; + + logger.log(`mindr to be removed: ${guid}`, { guid, mindrCount }); + + // + const mindrs = stateMindrs.filter(item => item.guid !== guid); + + const overdue = (stateOverdue || []).filter(mindr => mindr.guid !== guid); + const active = (stateActive || []).filter(mindr => mindr.guid !== guid); + + if ( + mindrCount.overdue === overdue.length && + mindrCount.active === active.length && + mindrCount.mindrs === mindrs.length + ) { + // + logger.log(`no mindr was removed, state remains untouched`); + + return state; + } + + const localState = { + ...state, + mindrs, + active, + overdue + }; + + return localState; +}; diff -Nru mailmindr-1.4.0/modules/store/selectors/index.mjs.js mailmindr-1.7.1/modules/store/selectors/index.mjs.js --- mailmindr-1.4.0/modules/store/selectors/index.mjs.js 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/modules/store/selectors/index.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,68 @@ +import { sanitizeHeaderMessageId } from '../../core-utils.mjs.js'; +import { createLogger } from '../../logger.mjs.js'; + +const logger = createLogger('modules/store/selectors'); + +export const selectMindrs = (/** @type {MailmindrState} */ state) => + state.mindrs || []; + +export const selectMindrByGuid = ( + /** @type {MailmindrState} */ state, + guid +) => { + const mindrs = selectMindrs(state); + const result = (mindrs || []).find(item => item.guid === guid); + + return result; +}; + +export const selectMindrByHeaderMessageId = ( + /** @type {MailmindrState} */ state, + headerMessageId +) => { + const mindrs = selectMindrs(state); + const result = (mindrs || []).find( + item => + sanitizeHeaderMessageId(item.headerMessageId) === + sanitizeHeaderMessageId(headerMessageId) + ); + + return result; +}; + +export const selectOpenDialogs = (/** @type {MailmindrState} */ state) => + state.openDialogs || []; + +export const selectDialogForType = ( + /** @type {MailmindrState} */ state, + dialogType +) => { + const openDialogs = selectOpenDialogs(state); + logger.log(selectDialogForType.name, { state, openDialogs }); + return openDialogs.find(dialog => dialog.dialogType === dialogType); +}; + +export const selectSettings = (/** @type {MailmindrState} */ state) => + state.settings; + +export const selectPresets = (/** @type {MailmindrState} */ state) => + state.presets; + +export const selectOpenConnections = (/** @type {MailmindrState} */ state) => + state.openConnections || []; + +export const selectDrafts = (/** @type {MailmindrState} */ state) => { + return state.__drafts || []; +}; + +export const selectDraftForSenderTabOrNull = ( + /** @type {MailmindrState} */ state, + /** @type {MindrDraft['sender']} */ sender +) => { + const drafts = selectDrafts(state); + const result = drafts.find( + ({ sender: { id, windowId } }) => + id === sender.id && windowId === sender.windowId + ); + return result || null; +}; diff -Nru mailmindr-1.4.0/modules/store/state-manager.mjs.js mailmindr-1.7.1/modules/store/state-manager.mjs.js --- mailmindr-1.4.0/modules/store/state-manager.mjs.js 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/modules/store/state-manager.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,142 @@ +import { createLogger } from '../logger.mjs.js'; + +const logger = createLogger('modules/store/state-manager'); + +export const createStore = (reducer, initialState) => { + logger.log(`Initializing state w/ `, { initialState }); + let localState = initialState; + let dispatching = false; + let isCorrupt = false; + let actions = []; + const handlers = { + change: new Set() + }; + + const defaultGetState = function() { + if (dispatching) { + throw new MailmindrStateError( + 'Cannot get state while in dispatching mode' + ); + } + return localState; + }; + + const defaultDispatch = function(action) { + if (dispatching) { + throw new MailmindrStateError( + 'Cannot update state while state is in dispatching mode' + ); + } + try { + dispatching = true; + logger.log(`★ start reduce '${action.type}'`, { + localState, + action + }); + actions.push({ name: action.type }); + localState = reducer(localState, action); + if (!localState) { + logger.error(`Action corrupted the state: ${action.type}`); + } + if (handlers.change.size) { + for (let handler of handlers.change.values()) { + setTimeout(() => handler(localState), 0); + } + } + logger.log(`★ end reduce '${action.type}'`, { localState }); + dispatching = false; + } catch (error) { + dispatching = false; + isCorrupt = true; + logger.error( + `Something went wrong during dispatching the action '${action.type}'`, + { + action, + actions, + error + } + ); + } + + return action; + }; + + const extendedDispatch = function(action) { + if (action && action.constructor && action.constructor.name) { + const constructorName = action.constructor.name.toLocaleLowerCase(); + switch (constructorName) { + case 'promise': + logger.warn('Promise as action?', action); + return action; + case 'asyncfunction': + return new Promise(async (success, failure) => { + try { + const result = await action( + extendedDispatch, + defaultGetState + ); + actions.push({ + name: `[async] ${action.name}` + }); + success(result); + } catch (asyncFunctionError) { + failure(asyncFunctionError); + } + }); + case 'function': + try { + actions.push({ name: `[func] ${action.name}` }); + return action(extendedDispatch, defaultGetState); + } catch (functionError) { + throw new MailmindrStateError( + `Error in function ${action.name}`, + functionError + ); + } + default: + return defaultDispatch(action); + } + } + }; + + let def = { + dispatch: extendedDispatch, + getState: defaultGetState, + addEventListener: (eventName, eventHandler) => { + const eventNameNormalized = eventName.toLocaleLowerCase(); + if (Object.keys(handlers).includes(eventNameNormalized)) { + if (handlers[eventNameNormalized].has(eventHandler)) { + logger.warn(`Handler for ${eventName} already defined.`); + } else { + handlers[eventNameNormalized].add(eventHandler); + } + } else { + logger.error( + `No event handler for event '${eventNameNormalized}' exist.` + ); + } + }, + removeEventListener: (eventName, eventHandler) => { + const eventNameNormalized = eventName.toLocaleLowerCase(); + if (Object.keys(handlers).includes(eventNameNormalized)) { + if (handlers[eventNameNormalized].has(eventHandler)) { + handlers[eventNameNormalized].delete(eventHandler); + } else { + logger.warn(`Handler for ${eventName} is not registered.`); + } + } else { + logger.error( + `No event handler for event '${eventNameNormalized}' exist.` + ); + } + } + }; + + return def; +}; + +export class MailmindrStateError extends Error { + constructor(...args) { + super(...args); + } +} diff -Nru mailmindr-1.4.0/modules/string-utils.mjs.js mailmindr-1.7.1/modules/string-utils.mjs.js --- mailmindr-1.4.0/modules/string-utils.mjs.js 2022-08-12 13:32:08.000000000 +0000 +++ mailmindr-1.7.1/modules/string-utils.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -1,6 +1,6 @@ import { createLogger } from './logger.mjs.js'; -const logger = createLogger('string-utils'); +const logger = createLogger('modules/string-utils'); const simplePluralize = (num, identifier) => { const pluralizer = new Intl.PluralRules(navigator.language, { diff -Nru mailmindr-1.4.0/modules/ui-utils.mjs.js mailmindr-1.7.1/modules/ui-utils.mjs.js --- mailmindr-1.4.0/modules/ui-utils.mjs.js 2022-08-12 13:32:08.000000000 +0000 +++ mailmindr-1.7.1/modules/ui-utils.mjs.js 2024-08-04 20:14:20.000000000 +0000 @@ -3,8 +3,9 @@ equalTimePresetValues } from './core-utils.mjs.js'; import { createLogger } from './logger.mjs.js'; +import { pluralize } from './string-utils.mjs.js'; -const logger = createLogger('ui-utils'); +const logger = createLogger('modules/ui-utils'); export const appendI18n = element => { if (!element) { @@ -61,19 +62,135 @@ }; export const selectDefaultActionPreset = (element, defaultActionPreset) => { - const availablePresets = Array.from(element.options) - .map(item => ({ - index: item.index, - actionPreset: JSON.parse(item.value) - })) + const presetsFromOptions = Array.from(element.options).map(item => ({ + index: item.index, + actionPreset: JSON.parse(item.value) + })); + const availablePresets = presetsFromOptions .filter(({ actionPreset }) => - equalActionPresetValues(actionPreset, defaultActionPreset) + // + areActionsEqual(actionPreset, defaultActionPreset, true) ) .map(({ index }) => index); const selectedIndex = availablePresets.shift() || 0; + element.selectedIndex = selectedIndex; }; +export const areActionsEqual = ( + someAction, + someOtherAction, + ignoreShowReminder = false +) => { + if (!someAction || !someOtherAction) { + return false; + } + + const props = [ + 'flag', + 'markUnread', + 'tagWithLabel', + 'copyMessageTo', + 'moveMessageTo', + ignoreShowReminder ? void 0 : 'showReminder' + ]; + + let result = true; + props.forEach(prop => { + let equal = someAction[prop] === someOtherAction[prop]; + if (!equal) { + logger.info( + `prop '${prop}' failed: '${someAction[prop]}' !== '${someOtherAction[prop]}'` + ); + result = result && false; + } + }); + + logger.info(`--- checks: ${result}`); + return result; +}; + +/** + * Selects the index of an action preset from a list of action presets + * @param {Array<{ readonly index: number; readonly actionPreset: MailmindrAction }>} list + * @param {MailmindrAction} defaultActionPreset + */ +export const selectDefaultActionPresetIndexFromList = ( + list, + defaultActionPreset +) => { + const availablePresets = list + .filter(({ actionPreset }) => + equalActionPresetValues(actionPreset, defaultActionPreset) + ) + .map(({ index }) => index); + const selectedIndex = availablePresets.shift() || 0; + + return selectedIndex; +}; + +export const selectDefaultRemindeMeMinutesBeforePreset = ( + element, + presets, + selectedRemindMeBeforeValue +) => { + const remindMeMinutesBefore = presets; + if (selectedRemindMeBeforeValue !== null) { + const selectedIndex = remindMeMinutesBefore.findIndex( + item => + parseInt(item.minutes, 10) === + parseInt(selectedRemindMeBeforeValue, 10) + ); + if (selectedIndex >= 0) { + element.selectedIndex = selectedIndex; + } + } +}; + +export const createRemindMeBeforePicker = ( + document, + element, + presets, + selectedRemindMeBeforeValue = null +) => { + const remindMeMinutesBefore = presets; + remindMeMinutesBefore.forEach(item => { + const option = document.createElement('option'); + const { minutes, unit, display: displayedValue } = item; + + option.value = String(minutes); + + if (unit === 'on-time') { + option.innerText = pluralize( + displayedValue, + 'mailmindr.utils.core.remindme.before.on-time' + ); + } else if (unit === 'minutes') { + option.innerText = pluralize( + displayedValue, + 'mailmindr.utils.core.remindme.before.minutes' + ); + } else if (unit === 'hours') { + option.innerText = pluralize( + displayedValue, + 'mailmindr.utils.core.remindme.before.hours' + ); + } else if (unit === 'no-reminder') { + option.innerText = pluralize( + displayedValue, + 'mailmindr.utils.core.remindme.before.no-reminder' + ); + } + element.appendChild(option); + }); + + selectDefaultRemindeMeMinutesBeforePreset( + element, + presets, + selectedRemindMeBeforeValue + ); +}; + // // // @@ -114,3 +231,8 @@ export const clearContents = parentElement => { Array.from(parentElement.children).forEach(child => child.remove()); }; + +export const isDarkMode = () => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + return mediaQuery.matches; +}; diff -Nru mailmindr-1.4.0/schema.json mailmindr-1.7.1/schema.json --- mailmindr-1.4.0/schema.json 2022-08-12 13:32:08.000000000 +0000 +++ mailmindr-1.7.1/schema.json 1970-01-01 00:00:00.000000000 +0000 @@ -1,20 +0,0 @@ -[ - { - "namespace": "mailmindrMessagesApi", - "functions": [ - { - "name": "openMessageByMessageHeaderId", - "type": "function", - "description": "Open message by given messageHeaderId.", - "async": true, - "parameters": [ - { - "name": "messageHeaderId", - "type": "string", - "description": "headerMessageId of the message that should be opened." - } - ] - } - ] - } -] diff -Nru mailmindr-1.4.0/scripts/mailmindr-background.js mailmindr-1.7.1/scripts/mailmindr-background.js --- mailmindr-1.4.0/scripts/mailmindr-background.js 2022-08-12 13:32:08.000000000 +0000 +++ mailmindr-1.7.1/scripts/mailmindr-background.js 2024-08-04 20:14:20.000000000 +0000 @@ -2,70 +2,66 @@ /// /// import { - createMindrFromActionTemplate, createMailmindrId, + createMindrFromActionTemplate, + createRemindMeMinutesBefore, createSystemActions, createSystemTimespans, - localFolderToGenericFolder, - isSameMindr, - equalTimePresetValues, + findThunderbirdVersion, genericFolderToLocalFolder, - getFlatFolderList, - isExecuted, - getActiveAndOverdueMindrs, - snoozeMindrs, - getGenericIceboxFolderForIdentityOrNull + getGenericIceboxFolderForIdentityOrNull, + localFolderToGenericFolder, + sanitizeHeaderMessageId, + sendConnectionMessageEx, + showReminderForMindr, + throttle } from '../modules/core-utils.mjs.js'; import { getCurrentStorageAdapterVersion, getStorageAdapter } from '../modules/storage.mjs.js'; import { createCorrelationId, createLogger } from '../modules/logger.mjs.js'; -import { applyActionToMessageInFolder } from '../modules/message-utils.mjs.js'; +import { createStore } from '../modules/store/state-manager.mjs.js'; +import { initialState } from '../modules/defaults.mjs.js'; +import { + closeDialog, + connectionClosed, + connectionOpened, + createOrUpdateDraft, + createOrUpdateMindr, + createTimespanPreset, + heartBeat, + heartBeatEx, + openDialog, + removeDraft, + removeMindr, + removeTimespanPreset, + setReplyReceived, + showMindrAlert, + snoozeMindrs, + updateSetting, + updateTimespanPreset +} from '../modules/store/actions/index.mjs.js'; +import { + selectDialogForType, + selectDraftForSenderTabOrNull, + selectMindrByGuid, + selectMindrByHeaderMessageId, + selectMindrs, + selectOpenConnections, + selectOpenDialogs, + selectPresets, + selectSettings +} from '../modules/store/selectors/index.mjs.js'; +import { + getMessages, + moveMessageToFolder +} from '../modules/message-utils.mjs.js'; +import rootReducer from '../modules/store/reducers/index.mjs.js'; const logger = createLogger('background'); -/** @type {MailmindrState} */ -let state = { - presets: { - actions: [], - time: [] - }, - settings: { - defaultActionPreset: { - copyMessageTo: null, - moveMessageTo: null, - text: null, - tagWithLabel: null, - flag: null, - isSystemAction: false, - markUnread: false, - showReminder: false - }, - defaultIceboxFolder: '', - defaultTimepreset: { - days: 0, - hours: 0, - minutes: 0, - isGenerated: true, - isRelative: false, - isSelectable: false, - text: null - }, - iceboxFolders: [], - snoozeTime: 15 - }, - mindrs: [], - active: [], - overdue: [], - openDialogs: [], - openConnections: [], - __inExecution: [] -}; - -const getState = () => { - return state; -}; +let store; // = createStore(() => {}, initialState); const createInitialSettings = (presets, _settings) => ({ snoozeTime: 15, // 15 minutes is initial default, @@ -74,23 +70,36 @@ defaultTimePreset: presets.time?.[presets.time?.find(item => item.isSelectable) || 0], // get first preset (if any) defaultActionPreset: - presets.actions?.[presets.actions?.find(item => item.isSelectable) || 0] // get first preset (if any) + presets.actions?.[ + presets.actions?.find(item => item.isSelectable) || 0 + ], // get first preset (if any) + defaultRemindMeMinutesBefore: 15 }); const createSystemPresets = () => ({ time: createSystemTimespans(), - actions: createSystemActions() + actions: createSystemActions(), + remindMeMinutesBefore: createRemindMeMinutesBefore() }); +const getStorage = async () => { + // + const storage = await browser.storage.local.get(null); + const storageVersion = storage?.storageVersion || 1; + + // + const { loadState, storeState } = getStorageAdapter(storageVersion); + return { loadState, storeState, storageVersion }; +}; + const initializeStorage = async () => { try { - // - const storage = await browser.storage.local.get(null); - const persistedStorageVersion = storage?.storageVersion || 1; - - // + const { + loadState, + storeState, + storageVersion: persistedStorageVersion + } = await getStorage(); const storageVersion = getCurrentStorageAdapterVersion(); - const { loadState, storeState } = getStorageAdapter(storageVersion); // if (storageVersion > persistedStorageVersion) { @@ -114,7 +123,8 @@ // const { time: systemGeneratedTimePresets, - actions + actions, + remindMeMinutesBefore } = createSystemPresets(); const defaultSettings = createInitialSettings({ systemGeneratedTimePresets, @@ -142,7 +152,8 @@ settings, presets: { time, - actions + actions, + remindMeMinutesBefore }, overdue: [], // can be computed active: [], // can be computed @@ -163,66 +174,13 @@ } }; -const getSettings = () => { - const { presets, settings } = getState(); - return { presets, settings }; -}; - -const onHeartBeat = async () => { - const windows = await messenger.windows.getAll({ - windowTypes: ['normal', 'app'] - }); - if (!windows.length) { - return; - } - - await dispatch('heartbeat'); - - const executeOverdueMindrs = async () => { - const { overdue, active, __inExecution } = getState(); - const overdueNotExecuted = overdue.filter(item => !item.isExecuted); - - if (Array.isArray(__inExecution) && __inExecution.length > 0) { - logger.warn( - `Mindr is executing (${__inExecution.length} in total), skipping further executions`, - { overdue, active, __inExecution } - ); - return; - } - - for (let mindr of overdueNotExecuted) { - const { guid } = mindr; - await dispatch('state:lock-execution', { guid }); - const executed = await executeMindr(mindr); - await dispatch('state:unlock-execution', { guid }); - if (executed) { - await dispatch('mindr:create-or-update', { - ...mindr, - isExecuted: true - }); - } - } - }; - - const showAlertDialog = async () => { - const { overdue, active } = getState(); - const overdueAndUnexecuted = overdue.filter( - mindr => - !isExecuted(mindr) && - String(mindr.remindMeMinutesBefore) !== '-1' - ); - - if (overdueAndUnexecuted.length > 0 || active.length > 0) { - await showMindrAlert({ overdue: overdueAndUnexecuted, active }); - } - }; - - await executeOverdueMindrs(); - await showAlertDialog(); +const onHeartBeatHandler = async () => { + store.dispatch(heartBeat()); + store.dispatch(heartBeatEx()); }; const startHeartbeat = () => { - setInterval(onHeartBeat, 1000 * 45); + setInterval(onHeartBeatHandler, 1000 * 45); }; const setMindrForCurrentMessage = async optionalCurrentMessage => { @@ -247,7 +205,7 @@ await showCreateOrUpdateMindrDialog(currentMessage, mindr); } } else { - console.warn('☠️ The current message cannot be determined.'); + logger.warn('☠️ The current message cannot be determined.'); } }; @@ -255,523 +213,17 @@ /* intentionally left blank */ }; -const createOrUpdateMindr = async (state, mindr) => { - const { mindrs: allMindrs, ...stateWithoutMindrs } = state; - const mindrsWithoutUpdatedMindr = allMindrs.filter( - item => !isSameMindr(item, mindr) - ); - const mindrs = [...mindrsWithoutUpdatedMindr, mindr]; - - const storageVersion = getCurrentStorageAdapterVersion(); - const { storeState } = getStorageAdapter(storageVersion); - - const localState = { - ...stateWithoutMindrs, - mindrs - }; - - await storeState(localState); - - return localState; -}; - -const executeMindr = async mindr => { - const { headerMessageId, metaData, action, guid } = mindr; - logger.log(`START executeMindr ${guid} w/ msgHdrId: '${headerMessageId}'`, { - guid, - headerMessageId - }); - const { - author, - subject, - folderAccountId, - folderName, - folderPath, - folderType, - folderAccountIdentityMailAddress - } = metaData; - const correlationId = createCorrelationId('executeMindr'); - const executionStart = Date.now(); - const { copyMessageTo, moveMessageTo } = action; - - const applyAction = async (messageId, action) => { - const { - flag, - markUnread, - showReminder, - tagWithLabel, - copyMessageTo, - moveMessageTo - } = action; - const messageProps = { - ...(flag && { flagged: true }), - ...(markUnread && { read: false }) - }; - - logger.info(`→ apply update to message ${messageId}`, messageProps); - - await messenger.messages.update(messageId, messageProps); - }; - - const destinationFolder = moveMessageTo - ? await genericFolderToLocalFolder(moveMessageTo) - : null; - const possibleSourceFolder = await genericFolderToLocalFolder({ - accountId: folderAccountId, - path: folderPath, - identityEmailAddress: folderAccountIdentityMailAddress - }); - const flatFolderList = await getFlatFolderList(); - const localFlatFolderList = await Promise.all( - flatFolderList - .filter(({ type }) => type === 'folder') - .map(async ({ folder }) => await genericFolderToLocalFolder(folder)) - ); - const folders = [ - possibleSourceFolder, - ...localFlatFolderList.filter( - fldr => - fldr.accountId !== possibleSourceFolder.accountId && - fldr.path !== possibleSourceFolder.path - ) - ]; - - logger.log(`BEGIN execution of ${mindr.guid}`, { - correlationId, - guid - }); - const startTime = performance.now(); - - const targetFolders = folders; - - let hasError = false; - let iterationCount = 0; - - logger.log(`BEGIN targetFolder iteration`, { - guid, - correlationId, - targetFolderCount: (targetFolders || []).length, - targetFolders - }); - - const applyActionToMessage = async (message, messageFolder) => { - const { id } = message; - - logger.log(`BEGIN applyActionToMessage`); - await applyAction(id, action); - logger.log( - `Do we have a destination folder? ${ - destinationFolder ? 'yes' : 'no' - }`, - destinationFolder - ); - if (destinationFolder) { - await doMoveMessageToFolder( - message, - destinationFolder, - correlationId - ); - } - logger.log(`END applyActionToMessage`); - }; - - const applyActionToFirstMessageInFolders = async () => { - for await (let folder of targetFolders) { - logger.log(` -- executeMindr: folder loop (${folder.name})`, { - correlationId, - folder - }); - - try { - const actionResult = await applyActionToMessageInFolder( - folder, - { headerMessageId, author }, - applyActionToMessage, - true - ); - const { done, value } = await actionResult.next(); - const success = Boolean(done && value && value.executed); - if (success) { - return true; - } - } catch (ex) { - logger.error('ERROR: execute mindr // mailmindr: >> !!', { - correlationId, - guid, - exception: ex - }); - hasError = true; - } - iterationCount++; - } - return false; - }; - - await applyActionToFirstMessageInFolders(); - - logger.log(`END targetFolder iteration`, { - correlationId, - guid, - targetFolderCount: (targetFolders || []).length, - targetFolders - }); - - const endTime = performance.now(); - logger.log( - `mailmindr: execution finished in ${endTime - startTime}ms`, - moveMessageTo - ); - - const executionEnd = Date.now(); - const executionDuration = (executionEnd - executionStart) / 1000; - - if (executionDuration > 3 * 60) { - logger.error( - `Execution of mindr '${guid}' took more than 180 seconds`, - { guid, correlationId, executionDuration } - ); - } else if (executionDuration > 60) { - logger.warn(`Execution of mindr '${guid}' took more than 60 seconds`, { - guid, - correlationId, - executionDuration - }); - } else { - logger.warn( - `Execution of mindr '${guid}' took ${executionDuration} seconds`, - { guid, correlationId, executionDuration } - ); - } - logger.log(`END execution of ${mindr.guid}`, { correlationId, guid }); - logger.log(`END executeMindr ${guid} w/ msgHdrId: '${headerMessageId}'`, { - guid, - headerMessageId - }); - - return !hasError; -}; - -const dispatch = async (action, payload) => { - const correlationId = createCorrelationId('dispatch'); - - const getUpdatedState = async (theAction, thePayload) => { - const storageVersion = getCurrentStorageAdapterVersion(); - const { storeState } = getStorageAdapter(storageVersion); - - try { - switch (theAction) { - case 'heartbeat': { - const { mindrs: mindrList } = getState(); - - const { - mindrs, - overdue, - active - } = getActiveAndOverdueMindrs(mindrList); - - // - // - // - - return { ...state, mindrs, active, overdue }; - } - case 'connection:open': { - const { port } = payload; - - const openConnections = [...state.openConnections, port]; - logger.log( - `open connections: ${openConnections.length}`, - openConnections - ); - return { ...state, openConnections }; - } - case 'connection:close': { - const { port } = payload; - const { name } = port; - - const openConnections = getState().openConnections.filter( - connection => connection.name !== name - ); - - return { ...state, openConnections }; - } - case 'dialog:open': { - const newDialogDetails = thePayload; - const openDialogs = [ - ...state.openDialogs, - newDialogDetails - ]; - return { ...state, openDialogs }; - } - case 'dialog:close': { - const { dialogId } = thePayload; - const { openDialogs: dialogs } = getState(); - const dialogDetails = dialogs.find( - dialogInfo => dialogId === dialogInfo.dialogId - ); - if (dialogDetails) { - const openDialogs = dialogs.filter( - openDialog => openDialog.dialogId !== dialogId - ); - logger.info( - `remaining open dialogs: ${openDialogs.length}` - ); - return { - ...state, - openDialogs - }; - } else { - logger.warn( - `cannot find details for open dialog ID: '${dialogId}'`, - { - payload: thePayload, - dialogId, - openDialogs: dialogs - } - ); - } - return state; - } - case 'state:initialize': - return await initializeStorage(); - case 'mindr:create-or-update': { - return await createOrUpdateMindr(state, thePayload); - } - case 'mindr:remove': { - const { guid } = thePayload; - const currentState = getState(); - const { - mindrs: stateMindrs, - overdue: stateOverdue, - active: stateActive - } = currentState; - - const mindrCount = { - mindrs: (stateMindrs || []).length, - overdue: (stateOverdue || []).length, - active: (stateActive || []).length - }; - - // - const mindrs = stateMindrs.filter( - item => item.guid !== guid - ); - - const overdue = (stateOverdue || []).filter( - mindr => mindr.guid !== guid - ); - const active = (stateActive || []).filter( - mindr => mindr.guid !== guid - ); - - if ( - mindrCount.overdue === overdue.length && - mindrCount.active === active.length && - mindrCount.mindrs === mindrs.length - ) { - // - logger.log( - `no mindr was removed, state remains untouched` - ); - - return getState(); - } - - const localState = { - ...currentState, - mindrs, - active, - overdue - }; - - await storeState(localState); - - return localState; - } - case 'preset:timespan-create': { - const currentState = getState(); - const { presets } = currentState; - const { time } = presets; - const { current } = thePayload; - - const localState = { - ...currentState, - presets: { - ...presets, - time: [...time, current] - } - }; - - await storeState(localState); - - return localState; - } - case 'preset:timespan-update': { - const currentState = getState(); - const { presets } = currentState; - const { time: timePresets } = presets; - const { current, source } = thePayload; - const time = timePresets.map(item => - equalTimePresetValues(item, source) ? current : item - ); - - logger.info(`new presets:`, time); - - const newState = { - ...currentState, - presets: { - ...presets, - time - } - }; - - await browser.storage.local.set({ - presets: newState.presets - }); - - return newState; - } - case 'preset:timespan-remove': { - const currentState = getState(); - const { presets } = currentState; - const { time: timePresets } = presets; - const { presets: toBeRemoved = [] } = thePayload; - - let time = [...timePresets]; - toBeRemoved.forEach(toBeRemovedPreset => { - time = time.filter( - preset => - !equalTimePresetValues( - preset, - toBeRemovedPreset - ) - ); - }); - - const newState = { - ...currentState, - presets: { - ...presets, - time - } - }; - - await browser.storage.local.set({ - presets: newState.presets - }); - - return newState; - } - case 'setting:update': { - const currentState = getState(); - const { name, value } = thePayload; - - const localState = { - ...currentState, - settings: { ...state.settings, [name]: value } - }; - - await storeState(localState); - - return localState; - } - case 'state:lock-execution': { - // - // - const currentState = getState(); - const { guid } = thePayload; - - const localState = { - ...currentState, - __inExecution: [...currentState.__inExecution, guid] - }; - - storeState(localState); - - return localState; - } - case 'state:unlock-execution': { - // - const currentState = getState(); - const { guid } = thePayload; - - const localState = { - ...currentState, - __inExecution: currentState.__inExecution.filter( - item => item !== guid - ) - }; - - storeState(localState); - - return localState; - } - } - } catch (exception) { - logger.error( - `ugh, we're compromising the state w/ msg '${theAction}' :`, - exception - ); - throw exception; - } - }; - - try { - logger.log(`:: acn :: ${action}`, { correlationId, action, payload }); - const mutatedState = await getUpdatedState(action, payload); - if (!mutatedState) { - logger.error(`:: acn :: ${action} failed`, { - correlationId, - action, - payload - }); - return false; - } - - state = mutatedState; - - try { - await refreshButtons(); - } catch (buttonUpdateException) { - /* there's silence */ - } - } catch (e) { - logger.error(`mailmindr crashed due to ${e.message}`, { - correlationId, - error: e - }); - - return false; - } - - return true; -}; - const sendConnectionMessage = async (connectionName, message) => { - const { openConnections } = getState(); - logger.info(`open connections?`, openConnections, 'send message', message); - const connection = openConnections.find(con => con.name === connectionName); - if (connection) { - await connection.postMessage(message); - } -}; - -const findDialogForType = dialogType => { - const { openDialogs } = getState(); - return openDialogs.find(dialog => dialog.dialogType === dialogType); -}; - -const findMindrByGuid = guid => { - const { mindrs } = getState(); - const result = (mindrs || []).find(item => item.guid === guid); - - return result; + const state = store.getState(); + const openConnections = selectOpenConnections(state); + await sendConnectionMessageEx(openConnections, message, connectionName); }; const editMindrByGuid = async guid => { const correlationId = createCorrelationId('editMindrByGuid'); logger.log('BEGIN editMindrByGuid', { correlationId, guid }); - const mindr = findMindrByGuid(guid); + const state = store.getState(); + const mindr = selectMindrByGuid(state, guid); const { headerMessageId, author, @@ -799,7 +251,8 @@ }; const bringDialogToFront = async dialogType => { - const dialog = await findDialogForType(dialogType); + const state = store.getState(); + const dialog = selectDialogForType(state, dialogType); if (!dialog) { return false; @@ -812,32 +265,6 @@ return true; }; -const closeCreateOrUpdateMindrDialog = async () => { - const correlationId = createCorrelationId('closeCreateOrUpdateMindrDialog'); - const dialogType = 'mailmindr:dialog:set-mindr'; - const dialog = findDialogForType(dialogType); - - logger.log(`BEGIN closeCreateOrUpdateMindrDialog`, { - correlationId, - dialogType, - dialog - }); - - if (dialog) { - await dispatch('dialog:close', { - dialogId: dialog.dialogId - }); - - await messenger.windows.remove(dialog.details.id); - } - - logger.log(`END closeCreateOrUpdateMindrDialog`, { - correlationId, - dialogType, - dialog - }); -}; - const showCreateOrUpdateMindrDialog = async (currentMessage, mindr) => { const dialogType = 'mailmindr:dialog:set-mindr'; const { headerMessageId, author, folder, subject } = currentMessage; @@ -846,13 +273,6 @@ logger.info('generic folder', genericFolder); const { accountId, name, path, type, identityEmailAddress } = genericFolder; - // - // - // - // - // - // - const dialogId = createMailmindrId('mailmindr:dialog:set-mindr'); const parameters = new URLSearchParams(); @@ -883,7 +303,7 @@ allowScriptsToClose: true }); - await dispatch('dialog:open', { dialogId, dialogType, details }); + store.dispatch(openDialog(dialogId, dialogType, details)); // // @@ -891,75 +311,6 @@ // }; -const showMindrAlert = async ({ overdue, active }) => { - const dialogType = 'mailmindr:mindr-alert'; - const dialog = findDialogForType(dialogType); - - if (dialog) { - logger.log('we already have a message dialog', dialog); - - if ((overdue || []).length === 0 && (active || []).length === 0) { - // - await dispatch('dialog:close', { - dialogId: dialog.details.id - }); - messenger.windows.remove(dialog.details.id); - } else { - // - await sendConnectionMessage('connection:mindr-alert', { - overdue, - active - }); - await messenger.windows.update(dialog.details.id, { - // - drawAttention: true - }); - } - } else { - logger.log( - 'need to open a new dialog with active/overdue mindrs', - active, - overdue - ); - if ((active || []).length === 0 && (overdue || []).length === 0) { - logger.log('no dialog needed'); - return; - } - const dialogId = createMailmindrId('mailmindr:dialog:mindr-alert'); - - const parameters = new URLSearchParams(); - parameters.set('dialogId', dialogId); - - const { width: screenWidth, availHeight: screenHeight } = screen; - const height = 200; - const width = 400; - const left = screenWidth - width; - const top = screenHeight - height; - const url = `/views/dialogs/mindr-alert/index.html?${parameters}`; - const details = await messenger.windows.create({ - left, - top, - height, - width, - url, - type: 'popup', - state: 'normal', - allowScriptsToClose: true - }); - - await messenger.windows.update(details.id, { - top, - left, - width, - height, - focused: true, - drawAttention: true - }); - - await dispatch('dialog:open', { dialogId, dialogType, details }); - } -}; - const showTimespanPresetEditor = async timePreset => { const dialogType = 'mailmindr:time-preset-editor'; const hasDialog = await bringDialogToFront(dialogType); @@ -988,33 +339,31 @@ allowScriptsToClose: true }); - await dispatch('dialog:open', { dialogId, dialogType, details }); + store.dispatch(openDialog(dialogId, dialogType, details)); }; const handleStartup = async () => { - const MAX_RETRIES = 5; - const initailze = async () => { - for (let retryCount = 0; retryCount < MAX_RETRIES; retryCount++) { - const success = await dispatch('state:initialize'); - if (success) { - return true; - } - await new Promise(resolve => setTimeout(() => resolve(), 1000)); - } - logger.error( - `Initialization failed after ${MAX_RETRIES} attempts. Aborting mailmindr.` - ); - return false; - }; + try { + const storedState = await initializeStorage(); + const localState = storedState || initialState; + + const { storeState } = await getStorage(); + + const saveState = async () => { + const state = store.getState(); + + await storeState(state); + }; + const saveStateThrottled = throttle(saveState, 250); + + store = createStore(rootReducer, localState); + store.addEventListener('change', saveStateThrottled); - const success = await initailze(); - if (success) { setupUI(); startHeartbeat(); - messenger.messageDisplayScripts.register({ - js: [{ file: '/scripts/mailmindr-message-script.js' }] - }); - } else { + } catch (startupError) { + logger.error(`mailmindr failed to start`, startupError); + // const errorButtonCaption = browser.i18n.getMessage( 'errorInitializationBrowserButtonText' ); @@ -1026,11 +375,16 @@ const { id, windowId } = tab; const msg = message || (await getCurrentDisplayedMessage({ id, windowId })); if (msg) { + logger.log(`findExistingMindrForTab`, store); const { headerMessageId } = msg; - const { mindrs } = getState(); + const sanitizedHeaderMessageId = sanitizeHeaderMessageId( + headerMessageId + ); + const { mindrs } = store.getState(); const mindr = mindrs.find( mindr => - mindr.headerMessageId === headerMessageId && !!headerMessageId + sanitizeHeaderMessageId(mindr.headerMessageId) === + sanitizedHeaderMessageId && !!sanitizedHeaderMessageId ); return mindr; @@ -1040,15 +394,22 @@ }; const handleSelectedMessagesChanged = async (tab, messageList) => { - const mindr = await findExistingMindrForTab(tab); + const { id, windowId } = tab; + const msg = await getCurrentDisplayedMessage({ id, windowId }); + if (!msg) { + logger.log(`handleSelectedMessagesChanged: no message selected, exit.`); + return; + } + + const mindr = await findExistingMindrForTab(tab, msg); const title = !!mindr ? '!' : null; const details = { text: title }; - const { id, windowId: _ } = tab; messenger.messageDisplayAction.setBadgeText(details); const tabInfo = await messenger.tabs.get(id); - if (!tabInfo.mailTab) { + if (!tabInfo.mailTab && tabInfo.type !== 'messageDisplay') { + logger.error(`this is not a mail tab`, tabInfo); return; } @@ -1057,19 +418,26 @@ ); if (!mindr) { + logger.log(`There's no mindr present for this tab`, { tabInfo, msg }); await messenger.tabs.executeScript(id, { code: `typeof removeExistingMessageBars === 'function' && removeExistingMessageBars()` }); return; } - messenger.messageDisplayAction.setBadgeBackgroundColor({ + await messenger.messageDisplayAction.setBadgeBackgroundColor({ color: '#ad3bff' }); - messenger.tabs.insertCSS(id, { + await messenger.tabs.insertCSS(id, { file: '/styles/message-bar.css' }); + + await messenger.tabs.executeScript(id, { + file: '/scripts/mailmindr-message-script.js' + // + }); await messenger.tabs.executeScript(id, { + // code: `createMindrBar('${JSON.stringify(mindr.guid)}')` }); }; @@ -1077,14 +445,6 @@ const getCurrentDisplayedMessage = async tab => { const correlationId = createCorrelationId('getCurrentDisplayedMessage'); const { id: tabId, windowId = messenger.windows.WINDOW_ID_CURRENT } = tab; - const query = { - windowId - }; - const tabs = await browser.tabs.query(query); - - if (!tabs || tabs.length === 0) { - return undefined; - } const message = await browser.messageDisplay.getDisplayedMessage(tabId); if (!message) { @@ -1133,16 +493,15 @@ return { ...message, - headerMessageId + headerMessageId: sanitizeHeaderMessageId(headerMessageId) }; } return message; }; -const refreshButtons = async () => { +const refreshButtons = async mindrs => { try { - const { mindrs } = getState(); const hasMindrs = mindrs.length; const current = await messenger.tabs.query({ currentWindow: true, @@ -1168,14 +527,22 @@ } }; -async function onLoadDialog(dialogOpenInfo) { +const onLoadDialog = dialogOpenInfo => { const { name, ...rest } = dialogOpenInfo; + const state = store.getState(); + + if (!store || !state) { + logger.error(`State '(${state})' or store '(${store})' isn't defined`, { + state, + store + }); + } switch (name) { case 'set-mindr': { - const { settings, presets, mindrs } = getState(); const { guid } = rest; - - const mindr = mindrs.find(mindr => mindr.guid === guid); + const settings = selectSettings(state); + const presets = selectPresets(state); + const mindr = selectMindrByGuid(state, guid); return { settings, @@ -1183,9 +550,30 @@ mindr }; } + case 'set-outgoing-mindr': { + const settings = selectSettings(state); + const presets = selectPresets(state); + const { + sender: { windowId, id } + } = rest; + const draft = selectDraftForSenderTabOrNull(state, { + windowId, + id + }); + + return { + settings, + presets, + draft + }; + } case 'mindr-alert': { - const { active, overdue } = getState(); - return { active, overdue }; + // + const { active, overdue } = store.getState(); + return { + active: active.filter(showReminderForMindr), + overdue: overdue.filter(showReminderForMindr) + }; } case 'time-preset-editor': { logger.log('REST', rest); @@ -1197,12 +585,16 @@ `mailmindr:onLoadDialog // no data loadable for dialog '${name}'` ); } -} +}; const onWindowRemoved = async windowId => { const log = logger.createContextLogger({ name: 'onWindowRemoved' }); - const { openDialogs } = getState(); + const state = store.getState(); + const openDialogs = selectOpenDialogs(state); + const settings = selectSettings(state); + const presets = selectPresets(state); + const dialogInfo = openDialogs.find( dialog => dialog.details.id === windowId ); @@ -1216,119 +608,35 @@ case 'mailmindr:time-preset-editor': await sendConnectionMessage('connection:mailmindr-options', { topic: 'settings:unlock', - message: { settings: getSettings() } + message: { settings: { settings, presets } } }); break; } - await dispatch('dialog:close', { dialogId }); + store.dispatch(closeDialog(dialogId)); } }; -const doMoveMessageToFolder = async ( - message, - destinationFolder, - parentCorrelationId -) => { - const correlationId = createCorrelationId( - 'doMoveMessageToFolder', - parentCorrelationId +const findIceboxFolderForAccountIdentityMailAddress = async identityMailAddress => { + const { settings } = store.getState(); + const defaultIceboxFolder = await getGenericIceboxFolderForIdentityOrNull( + identityMailAddress, + settings ); - try { - const { id } = message; - const { accountId, path } = destinationFolder; - logger.log(`BEGIN move message ${id}`, { - correlationId, - message - }); - await messenger.messages.move([id], { accountId, path }); - logger.log(`END move message ${id}`, { - correlationId, - message - }); - return true; - } catch (error) { - logger.error(error, { correlationId }); - return false; - } -}; - -const moveMessageToFolder = async ( - messageDetails, - sourceFolder, - targetFolder -) => { - const correlationId = createCorrelationId('moveMessagesToFolder'); - try { - logger.info('moveMessageToFolder target', { - correlationId, - targetFolder - }); - - const moveMessageToFolderAction = async ( - message, - _messageSourceFolder - ) => { - await doMoveMessageToFolder(message, targetFolder, correlationId); - }; - - logger.info(`BEGIN applying action to folder`, { - correlationId, - sourceFolder, - messageDetails - }); - const actionResult = await applyActionToMessageInFolder( - sourceFolder, - messageDetails, - moveMessageToFolderAction, - true - ); - const { done, value } = await actionResult.next(); - - // - const success = Boolean(done); - - const logMessage = `moveMessageToFolder : applyActionToMessageInFolder returns { done: ${done}, value: ${value} }`; - if (success) { - logger.info(logMessage, { correlationId, result: { done, value } }); - } else { - logger.warn(logMessage, { correlationId, result: { done, value } }); - } - - if (value === null) { - logger.error( - `moveMessageToFolder : applyActionToMessageInFolder message not found in folder` - ); - } - logger.info(`END applying action to folder`, { - correlationId, - sourceFolder, - messageDetails - }); - } catch (e) { - logger.error(`moveMessageToFolder failed: ${e.message}`, { - correlationId, - error: e - }); - return false; - } - - return true; + return defaultIceboxFolder; }; const moveToIcebox = async (headerMessageId, metaData) => { - const { settings } = getState(); - const { folderAccountIdentityMailAddress } = metaData; - const defaultIceboxFolder = await getGenericIceboxFolderForIdentityOrNull( - folderAccountIdentityMailAddress, - settings + const defaultIceboxFolder = await findIceboxFolderForAccountIdentityMailAddress( + folderAccountIdentityMailAddress ); if (!defaultIceboxFolder) { return false; } + const correlationId = createCorrelationId('moveToIcebox'); logger.log('BEGIN moveToIcebox', { correlationId, @@ -1367,26 +675,30 @@ const messageHandler = async (request, _sender, _sendResponse) => { const { action, payload } = request; + let result = null; switch (action) { case 'dialog:open': result = { status: 'ok', - payload: await onLoadDialog(payload) + payload: onLoadDialog(payload) }; return result; case 'dialog:close': - const dialogId = payload; - const { openDialogs } = getState(); - const dialog = openDialogs.find( - dialog => dialog.dialogId === dialogId - ); + { + const dialogId = payload; + const state = store.getState(); + const openDialogs = selectOpenDialogs(state); + const dialog = openDialogs.find( + dialog => dialog.dialogId === dialogId + ); - await dispatch('dialog:close', { dialogId }); + store.dispatch(closeDialog(dialogId)); - return { status: 'ok', dialog }; + return { status: 'ok', dialog }; + } break; case 'dialog:bring-to-front': const { dialogType } = payload; @@ -1397,14 +709,39 @@ return { status: 'error', paylad: null }; } break; - case 'mindrs:list': + case 'mindrs:list': { + const state = store.getState(); + const mindrsList = selectMindrs(state); result = { status: 'ok', payload: { - mindrs: state.mindrs + mindrs: mindrsList } }; return result; + } + case 'mindr:create-outgoing': { + const correlationId = createCorrelationId( + `messageHandler:mindr:create` + ); + logger.log(`BEGIN messageHandler:mindr:create-outgoing`, { + correlationId, + payload + }); + + store.dispatch(createOrUpdateDraft(payload)); + + const result = { + status: 'ok', + payload: {} + }; + logger.log(`END messageHandler:mindr:create-outgoing`, { + correlationId, + payload + }); + + return result; + } case 'mindr:create': { const { guid, headerMessageId, metaData, doMoveToIcebox } = payload; const correlationId = createCorrelationId( @@ -1431,7 +768,11 @@ // // if (!createdMindr.action.moveMessageTo) { - const existingMindr = findMindrByGuid(guid); + const state = store.getState(); + const existingMindr = selectMindrByGuid( + state, + guid + ); if (existingMindr) { logger.log( `updating existing mindr, set icebox folder from original mindr`, @@ -1457,9 +798,9 @@ }; // - const { - settings: { defaultIceboxFolder } - } = getState(); + const defaultIceboxFolder = await findIceboxFolderForAccountIdentityMailAddress( + metaData.folderAccountIdentityMailAddress + ); const iceBoxFolder = await genericFolderToLocalFolder( defaultIceboxFolder ); @@ -1481,7 +822,7 @@ } } - await dispatch('mindr:create-or-update', createdMindr); + store.dispatch(createOrUpdateMindr(createdMindr)); result = { status: createdMindr ? 'ok' : 'error', @@ -1491,7 +832,9 @@ }; logger.log('refresh buttons', { correlationId }); - await refreshButtons(); + const state = store.getState(); + + await refreshButtons(selectMindrs(state)); // } catch (error) { logger.error(`FAIL messageHandler:mindr:create`, { @@ -1508,9 +851,18 @@ return result; } + case 'mindr:remove-outgoing-mindr': { + const { sender } = payload; + const state = store.getState(); + const draft = selectDraftForSenderTabOrNull(state, sender); + + store.dispatch(removeDraft(draft)); + + return { status: 'ok', paylad: null }; + } case 'mindr:remove': { const { guid } = payload; - await dispatch('mindr:remove', { guid }); + store.dispatch(removeMindr(guid)); return { status: 'ok', paylad: null }; } @@ -1518,7 +870,7 @@ logger.log('mindr:get-information', payload); const { guid } = payload; - const { mindrs } = getState(); + const { mindrs } = store.getState(); const mindr = mindrs.find(mindr => mindr.guid === guid); logger.log(`mindr:get-information: ${mindr.headerMessageId}`); @@ -1531,25 +883,34 @@ break; } case 'navigate:open-message-by-mindr-guid': - // - // - const { guid } = payload; - const { mindrs } = getState(); - const mindr = mindrs.find(mindr => mindr.guid === guid); - if (mindr) { - const { headerMessageId } = mindr; - if (messenger.messageDisplay.open) { - await messenger.messageDisplay.open({ - headerMessageId, - active: true - }); - } else { - await browser.mailmindrMessagesApi.openMessageByMessageHeaderId( - headerMessageId + { + // + // + const { guid } = payload; + const state = store.getState(); + const mindr = selectMindrByGuid(state, guid); + + if (mindr) { + const headerMessageId = sanitizeHeaderMessageId( + mindr.headerMessageId ); + if (messenger.messageDisplay.open) { + logger.log( + `Open message by headerMessageId [nativeAPI] < '${headerMessageId}' >` + ); + try { + await messenger.messageDisplay.open({ + headerMessageId, + active: true + }); + } catch (openMessageError) { + logger.error( + `The message cannot be opened`, + openMessageError + ); + } + } } - - // } break; case 'refresh:buttons': @@ -1561,24 +922,30 @@ `messageHandler:mindr:snooze` ); - const { mindrs } = getState(); - const { presets, settings } = getSettings(); + const localState = store.getState(); + const mindrs = selectMindrs(localState); + const presets = selectPresets(localState); + const settings = selectSettings(localState); + const { snoozeTime } = settings; - await snoozeMindrs( - dispatch, - list, - mindrs, + logger.log(`mindr:snooze // snoozeTime: ${snoozeTime}`, { snoozeTime, - correlationId + mindrs + }); + + store.dispatch( + snoozeMindrs(list, mindrs, snoozeTime, correlationId) ); - await dispatch('heartbeat'); + store.dispatch(heartBeat()); - const { active, overdue } = getState(); + const { active, overdue } = store.getState(); const overdueNotExecuted = overdue.filter(item => !item.isExecuted); - await showMindrAlert({ overdue: overdueNotExecuted, active }); + store.dispatch( + showMindrAlert({ overdue: overdueNotExecuted, active }) + ); return { status: 'ok', paylad: null }; } @@ -1588,13 +955,15 @@ do { const guid = list.pop(); - await dispatch('mindr:remove', { guid }); + store.dispatch(removeMindr(guid)); } while (list.length); - const { active, overdue } = getState(); + const { active, overdue } = store.getState(); const overdueNotExecuted = overdue.filter(item => !item.isExecuted); - await showMindrAlert({ overdue: overdueNotExecuted, active }); + store.dispatch( + showMindrAlert({ overdue: overdueNotExecuted, active }) + ); return { status: 'ok', paylad: null }; } @@ -1604,39 +973,53 @@ if (source) { // logger.warn(`updating timespan`, current, source); - await dispatch('preset:timespan-update', { current, source }); + store.dispatch(updateTimespanPreset(current, source)); } else { // logger.info(`creating new timespan`, current); - await dispatch('preset:timespan-create', { current }); + store.dispatch(createTimespanPreset(current)); } + const state = store.getState(); + const settings = selectSettings(state); + const presets = selectPresets(state); + await sendConnectionMessage('connection:mailmindr-options', { topic: 'settings:unlock', - message: { settings: getSettings() } + message: { settings: { settings, presets } } }); return { status: 'ok', paylad: null }; } case 'preset:remove-timespans': { - const { presets } = payload; + const { presets: currentPresets } = payload; + + if ( + currentPresets && + Array.isArray(currentPresets) && + currentPresets.length + ) { + store.dispatch(removeTimespanPreset(currentPresets)); + const state = store.getState(); + const settings = selectSettings(state); + const presets = selectPresets(state); - if (presets && Array.isArray(presets) && presets.length) { - await dispatch('preset:timespan-remove', { presets: presets }); await sendConnectionMessage('connection:mailmindr-options', { topic: 'settings:updated', - message: { settings: getSettings() } + message: { settings: { settings, presets } } }); } return { status: 'ok', paylad: null }; } case 'options:get-settings': { - const payload = getSettings(); + const state = store.getState(); + const settings = selectSettings(state); + const presets = selectPresets(state); await sendConnectionMessage('connection:mailmindr-options', { topic: 'settings:updated', - message: { settings: payload } + message: { settings: { settings, presets } } }); break; } @@ -1644,7 +1027,8 @@ let { name, value } = payload; logger.info(`→ set '${name}' to '${value}'`, value); - const { settings } = getSettings(); + const state = store.getState(); + const settings = selectSettings(state); const propNames = Object.keys(settings); switch (name) { @@ -1681,13 +1065,14 @@ } if (propNames.indexOf(name) >= 0) { - await dispatch('setting:update', { name, value }); - - const payload = getSettings(); + store.dispatch(updateSetting(name, value)); + const state = store.getState(); + const settings = selectSettings(state); + const presets = selectPresets(state); await sendConnectionMessage('connection:mailmindr-options', { topic: 'settings:updated', - message: { settings: payload } + message: { settings: { settings, presets } } }); } else { logger.error( @@ -1697,9 +1082,12 @@ break; } case 'settings:force-unlock': { + const state = store.getState(); + const settings = selectSettings(state); + const presets = selectPresets(state); await sendConnectionMessage('connection:mailmindr-options', { topic: 'settings:unlock', - message: { settings: getSettings() } + message: { settings: { settings, presets } } }); break; } @@ -1710,8 +1098,9 @@ } case 'do:mindr-action-remove': { const { guid } = payload; - const { mindrs } = getState(); - const mindr = mindrs.find(mindr => mindr.guid === guid); + const state = store.getState(); + const mindr = selectMindrByGuid(state, guid); + if (!mindr) { return; } @@ -1743,7 +1132,10 @@ } // - if (state.mindrs.length) { + const localState = store.getState(); + const mindrs = selectMindrs(localState); + + if (mindrs.length) { messenger.browserAction.enable(); } else { messenger.browserAction.disable(); @@ -1771,63 +1163,71 @@ return; } + logger.log(`connectionHandler called for ${name}`); if (name === 'connection:mindr-alert') { port.onMessage.addListener(onConnectionMessage); - port.onDisconnect.addListener( - async () => await dispatch('connection:close', { port }) + port.onDisconnect.addListener(() => + store.dispatch(connectionClosed(port)) ); - await dispatch('connection:open', { port }); + store.dispatch(connectionOpened(port)); // - const { overdue, active } = getState(); + const { overdue, active } = store.getState(); await sendConnectionMessage(name, { - overdue, - active + overdue: overdue.filter(showReminderForMindr), + active: active.filter(showReminderForMindr) }); } else if (name === 'connection:mailmindr-options') { port.onMessage.addListener(onConnectionMessage); - port.onDisconnect.addListener( - async () => await dispatch('connection:close', { port }) + port.onDisconnect.addListener(() => + store.dispatch(connectionClosed(port)) ); - await dispatch('connection:open', { port }); - - // - - // - // - // - // + store.dispatch(connectionOpened(port)); } }; browser.tabs.onMoved.addListener(async ({}) => {}); browser.tabs.onActivated.addListener(async activeInfo => { + if (!store) { + logger.error(`tabs.onActivated:handler: store is undefined`); + return; + } + const { tabId, windowId } = activeInfo || {}; - console.log(`tab activated: ${tabId}, ${windowId} <- ${activeInfo}`); - await refreshButtons(); + const { mindrs } = store.getState(); + // + await refreshButtons(mindrs); }); browser.mailTabs.onSelectedMessagesChanged.addListener( handleSelectedMessagesChanged ); -browser.messageDisplayAction.onClicked.addListener(async ({ id, windowId }) => { - const currentMessage = await getCurrentDisplayedMessage({ id, windowId }); - const mindr = await findExistingMindrForTab( - { +browser.messageDisplayAction.onClicked.addListener( + async ({ id, windowId, type }) => { + const currentMessage = await getCurrentDisplayedMessage({ id, windowId - }, - currentMessage - ); + }); + const mindr = await findExistingMindrForTab( + { + id, + windowId + }, + currentMessage + ); - await showCreateOrUpdateMindrDialog(currentMessage, mindr); - // -}); + await showCreateOrUpdateMindrDialog(currentMessage, mindr); + // + } +); browser.menus.onShown.addListener(info => { - const contexts = ['message_list', 'page']; + const /** @type {browser.menus.ContextType[]} */ contexts = [ + 'message_list', + 'page' + ]; // if ( info && @@ -1871,6 +1271,77 @@ messageHandler ); +// +// +// +// +// +// +// +// +// + +messenger.compose.onBeforeSend.addListener((tab, details) => { + logger.log(`ONBEFORESEND -- `, { tab, details }); + return { cancel: false }; +}); + +messenger.compose.onAfterSend.addListener(async (tab, sendInfo) => { + const { messages, mode, error, headerMessageId } = sendInfo; + + const findHeaderMessageId = ({ messages, headerMessageId }) => { + if (headerMessageId) { + logger.log(`🧠 sent headerMessageId: ${headerMessageId}`); + return headerMessageId; + } else if (messages && messages.length) { + const message = messages[0]; + return message.headerMessageId; + } else { + return null; + } + }; + + const hdrMsgId = findHeaderMessageId({ messages, headerMessageId }); + if (hdrMsgId) { + // + const maybeDraft = selectDraftForSenderTabOrNull(store.getState(), tab); + if (maybeDraft && maybeDraft.mindr) { + const { mindr } = maybeDraft; + const { + author = '', + subject = '', + folder = null + } = sendInfo?.messages?.[0]; + const sourceFolder = await localFolderToGenericFolder(folder); + const { + accountId: folderAccountId, + identityEmailAddress: folderAccountIdentityMailAddress, + name: folderName, + path: folderPath, + type: folderType + } = sourceFolder; + const createdMindr = await createMindrFromActionTemplate({ + ...mindr, + headerMessageId, + metaData: { + headerMessageId, + author, + subject, + folderAccountId, + folderName, + folderPath, + folderType, + folderAccountIdentityMailAddress + } + }); + + store.dispatch(createOrUpdateMindr(createdMindr)); + } + } + + logger.log(`ONAFTERSEND -- `, { tab, sendInfo }); +}); + browser.runtime.onConnect.addListener(connectionHandler); browser.commands.onCommand.addListener(async command => { @@ -1885,4 +1356,56 @@ } }); +const thunderbirdVersion = findThunderbirdVersion(); +const handleNewMailReceived = async ( + /** @type {messenger.folders.MailFolder} */ folder, + /** @type {messenger.messages.MessageList} */ messages +) => { + const /** @type {(mindr: string) => Mindr} */ selectByHeaderMessageId = selectMindrByHeaderMessageId.bind( + null, + store.getState() + ); + const /** @type { { messageHeaderId: string, replyToHeaderMessageId: string }[] } */ headerMessageIds = []; + for await (let msg of getMessages(messages)) { + const messageId = msg.id; + const message = await messenger.messages.getFull(messageId); + // + const { headers } = message; + const messageHeaderId = msg.headerMessageId; + const inReplyTo = headers?.['in-reply-to']; + if (inReplyTo) { + headerMessageIds.push( + ...inReplyTo.map(item => ({ + messageHeaderId, + replyToHeaderMessageId: sanitizeHeaderMessageId(item) + })) + ); + } + } + + const mindrs = headerMessageIds + .map(({ replyToHeaderMessageId, messageHeaderId }) => { + const mindr = selectByHeaderMessageId(replyToHeaderMessageId); + if (mindr) { + return { mindr, messageHeaderId }; + } else return undefined; + }) + .filter(Boolean); + + await Promise.all( + mindrs.map(async ({ mindr, messageHeaderId }) => { + await store.dispatch(setReplyReceived(mindr, messageHeaderId)); + }) + ); +}; + +if (thunderbirdVersion <= 121) { + browser.messages.onNewMailReceived.addListener(handleNewMailReceived); +} else { + browser.messages.onNewMailReceived.addListener( + handleNewMailReceived, + false + ); +} + handleStartup(); diff -Nru mailmindr-1.4.0/scripts/mailmindr-message-script.js mailmindr-1.7.1/scripts/mailmindr-message-script.js --- mailmindr-1.4.0/scripts/mailmindr-message-script.js 2022-08-12 13:32:08.000000000 +0000 +++ mailmindr-1.7.1/scripts/mailmindr-message-script.js 2024-08-04 20:14:20.000000000 +0000 @@ -1,4 +1,4 @@ -const removeExistingMessageBars = () => { +var removeExistingMessageBars = () => { const messageBars = document.querySelectorAll('.mailmindr-message-bar'); if (!messageBars) { return; @@ -11,10 +11,10 @@ /** @typedef {() => HTMLDivElement} getExistingessageBarFunction */ /** @type {getExistingessageBarFunction} */ -const getExistingMessageBar = () => +var getExistingMessageBar = () => document.querySelector('.mailmindr-message-bar'); -const createUI = (guid, dueDateTimeText) => { +var createUI = (guid, dueDateTimeText) => { const mailmindrBar = document.createElement('div'); mailmindrBar.className = 'mailmindr-message-bar'; @@ -54,6 +54,21 @@ }); }); + const mailmindrRemoveBtn = document.createElement('button'); + mailmindrRemoveBtn.className = 'mailmindr-button mailmindr-button--micro'; + mailmindrRemoveBtn.innerText = browser.i18n.getMessage( + 'view.message-display.notification.button.remove' + ); + mailmindrRemoveBtn.addEventListener('click', async () => { + await messenger.runtime.sendMessage({ + action: 'do:mindr-action-remove', + payload: { + guid + } + }); + }); + + mailmindrButtonWrapper.appendChild(mailmindrRemoveBtn); mailmindrButtonWrapper.appendChild(mailmindrEditBtn); mailmindrButtonWrapper.appendChild(mailmindrCloseBtn); @@ -68,7 +83,7 @@ window.document.body.insertBefore(mailmindrBar, messageWrapper); }; -const createMindrBar = async guid => { +var createMindrBar = async guid => { const mindrGuid = JSON.parse(guid) .replace(/"/g, '') .replace(/'/g, ''); diff -Nru mailmindr-1.4.0/views/dialogs/create-mindr/index.css mailmindr-1.7.1/views/dialogs/create-mindr/index.css --- mailmindr-1.4.0/views/dialogs/create-mindr/index.css 2022-08-12 13:32:09.000000000 +0000 +++ mailmindr-1.7.1/views/dialogs/create-mindr/index.css 2024-08-04 20:14:20.000000000 +0000 @@ -23,7 +23,9 @@ } input[type='checkbox'] { - display: none; + /* display: none; */ + height: 16px; + width: 16px; } input[type='checkbox'] + label { @@ -34,7 +36,7 @@ color: silver; } -input[type='checkbox'] + label::before { +/* input[type='checkbox'] + label::before { content: ''; display: block; float: left; @@ -43,17 +45,19 @@ border: 1px solid rgba(12, 12, 13, 0.1); border-radius: 2px; background-color: var(--grey-90-a10); -} +} */ -input[type='checkbox']:checked + label::before { +/* input[type='checkbox']:checked + label::before { background-image: url(chrome://global/skin/icons/check.svg); + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAHklEQVQ4T2NkoBAwUqifYdQAhtEwACai0XQwGMIAACaYABGnE9aRAAAAAElFTkSuQmCC'); background-position: center; background-repeat: no-repeat; -} + background-color: blue; +} */ -input[type='checkbox'] + label:hover::before { +/* input[type='checkbox'] + label:hover::before { background-color: var(--grey-90-a20); -} +} */ /* input[type='checkbox']:checked:hover + label::before { background-color: var(--blue-70); @@ -130,6 +134,15 @@ } @media (prefers-color-scheme: dark) { + * { + color: #f9f9fa; + } + + body { + color: #f9f9fa; + background-color: #2a2a2e; + } + input[type='text'], input[type='number'], select { diff -Nru mailmindr-1.4.0/views/dialogs/create-mindr/index.js mailmindr-1.7.1/views/dialogs/create-mindr/index.js --- mailmindr-1.4.0/views/dialogs/create-mindr/index.js 2022-08-12 13:32:09.000000000 +0000 +++ mailmindr-1.7.1/views/dialogs/create-mindr/index.js 2024-08-04 20:14:20.000000000 +0000 @@ -6,11 +6,11 @@ } from '../../../modules/core-utils.mjs.js'; import { clearContents, + createRemindMeBeforePicker, createTimePresetPicker, selectDefaultActionPreset, selectDefaultTimePreset } from '../../../modules/ui-utils.mjs.js'; -import { pluralize } from '../../../modules/string-utils.mjs.js'; import { createCorrelationId, createLogger @@ -104,37 +104,46 @@ } = getDialogParameters(); const remindMe = document.getElementById('mailmindr--preset-remind-me'); - const remindMeMinutesBefore = - /** @type {HTMLInputElement} */ (remindMe).value || 0; + const remindMeMinutesBefore = parseInt( + /** @type {HTMLInputElement} */ (remindMe).value || '0', + 10 + ); const due = getDateTimefromPickers(); - const actionTemplate = getActionTemplateFromPicker(); + const actionTemplate = /** @type {} */ getActionTemplateFromPicker(); + if (typeof remindMeMinutesBefore === 'number') { + if (remindMeMinutesBefore >= 0) { + actionTemplate.showReminder = true; + } else { + actionTemplate.showReminder = false; + } + } const iceBox = /** @type {HTMLInputElement} */ (document.getElementById( 'mailmindr--icebox' )); const doMoveToIcebox = iceBox.disabled ? false : iceBox.checked; - + const payload = { + guid, + headerMessageId, + due, + remindMeMinutesBefore, + actionTemplate, + notes: '', + metaData: { + headerMessageId, + author, + subject, + folderAccountId, + folderName, + folderPath, + folderType, + folderAccountIdentityMailAddress + }, + doMoveToIcebox + }; const result = await messenger.runtime.sendMessage({ action: 'mindr:create', - payload: { - guid, - headerMessageId, - due, - remindMeMinutesBefore, - actionTemplate, - notes: '', - metaData: { - headerMessageId, - author, - subject, - folderAccountId, - folderName, - folderPath, - folderType, - folderAccountIdentityMailAddress - }, - doMoveToIcebox - } + payload }); doCancel(); }; @@ -280,7 +289,7 @@ const { payload: { settings, - presets: { time, actions }, + presets: { time, actions, remindMeMinutesBefore }, mindr } } = result; @@ -325,7 +334,15 @@ const moveToIceBox = /** @type {HTMLInputElement} */ (document.getElementById( 'mailmindr--icebox' )); - moveToIceBox.disabled = editMindr || !isIceboxFolderSet; + + if (editMindr) { + moveToIceBox.disabled = true; + } else if (!isIceboxFolderSet) { + moveToIceBox.disabled = true; + } else { + moveToIceBox.disabled = false; + } + moveToIceBox.checked = editMindr ? sourceFolderIsIceboxFolder : isIceboxFolderSet; @@ -425,46 +442,13 @@ )} | mailmindr`; } - const remindMeMinutesBefore = [ - { minutes: 0, display: 0, unit: 'on-time' }, - { minutes: 5, display: 5, unit: 'minutes' }, - { minutes: 15, display: 15, unit: 'minutes' }, - { minutes: 30, display: 30, unit: 'minutes' }, - { minutes: 60, display: 1, unit: 'hours' }, - { minutes: 120, display: 2, unit: 'hours' }, - { minutes: 240, display: 4, unit: 'hours' }, - { minutes: -1, display: null, unit: 'no-reminder' } - ]; const remindMe = document.getElementById('mailmindr--preset-remind-me'); - remindMeMinutesBefore.forEach(item => { - const option = document.createElement('option'); - const { minutes, unit, display: displayedValue } = item; - - option.value = String(minutes); - - if (unit === 'on-time') { - option.innerText = pluralize( - displayedValue, - 'mailmindr.utils.core.remindme.before.on-time' - ); - } else if (unit === 'minutes') { - option.innerText = pluralize( - displayedValue, - 'mailmindr.utils.core.remindme.before.minutes' - ); - } else if (unit === 'hours') { - option.innerText = pluralize( - displayedValue, - 'mailmindr.utils.core.remindme.before.hours' - ); - } else if (unit === 'no-reminder') { - option.innerText = pluralize( - displayedValue, - 'mailmindr.utils.core.remindme.before.no-reminder' - ); - } - remindMe.appendChild(option); - }); + createRemindMeBeforePicker( + document, + remindMe, + remindMeMinutesBefore, + settings.defaultRemindMeMinutesBefore + ); translateDocument(document); diff -Nru mailmindr-1.4.0/views/dialogs/mindr-alert/index.js mailmindr-1.7.1/views/dialogs/mindr-alert/index.js --- mailmindr-1.4.0/views/dialogs/mindr-alert/index.js 2022-08-12 13:32:09.000000000 +0000 +++ mailmindr-1.7.1/views/dialogs/mindr-alert/index.js 2024-08-04 20:14:20.000000000 +0000 @@ -24,6 +24,15 @@ // +const openMindrByGuid = async guid => { + await messenger.runtime.sendMessage({ + action: 'navigate:open-message-by-mindr-guid', + payload: { + guid + } + }); +}; + const onMindrClick = mindr => { const headerItem = document.getElementsByTagName('header').item(0); [...headerItem.childNodes].forEach(child => child.remove()); @@ -31,12 +40,7 @@ headerItem.appendChild(item); item.addEventListener('click', async () => { - await messenger.runtime.sendMessage({ - action: 'navigate:open-message-by-mindr-guid', - payload: { - guid: mindr.guid - } - }); + await openMindrByGuid(mindr.guid); }); selectListItem(mindr); @@ -105,6 +109,7 @@ mindrs.forEach(mindr => { const item = createListItem(mindr); item.onclick = () => onMindrClick(mindr); + item.ondblclick = () => openMindrByGuid(mindr.guid); listElement.appendChild(item); }); }; @@ -187,7 +192,7 @@ const { active, overdue } = message; const selectedGuid = findSelectedGuid(); - logger.log(`active: `, active, `overdue: `, overdue); + logger.log(`alert -- active/overdue: `, { active, overdue }); const mindrs = [...overdue, ...active].filter( (item, index, list) => list.findIndex(value => value.guid === item.guid) === index @@ -209,7 +214,7 @@ name: 'connection:mindr-alert' }); - await port.postMessage({ + const _result = await port.postMessage({ action: 'dialog:open', payload: { name: 'mindr-alert' } }); @@ -255,6 +260,7 @@ }; document.addEventListener('DOMContentLoaded', onLoad, { once: true }); + document.addEventListener('keydown', async event => { if (event.code === 'Escape') { clearList(); diff -Nru mailmindr-1.4.0/views/options/index.html mailmindr-1.7.1/views/options/index.html --- mailmindr-1.4.0/views/options/index.html 2022-08-12 13:32:08.000000000 +0000 +++ mailmindr-1.7.1/views/options/index.html 2024-08-04 20:14:20.000000000 +0000 @@ -144,6 +144,25 @@
+ + +
+
+ +
+
+
+
+ { const { presets, settings } = options; - const { time, actions } = presets; + const { time, actions, remindMeMinutesBefore } = presets; displayTimePresets(time); @@ -156,6 +157,7 @@ defaultTimePreset, defaultActionPreset, defaultIceboxFolder, + defaultRemindMeMinutesBefore, snoozeTime } = settings; @@ -177,6 +179,16 @@ selectDefaultActionPreset(defaultActionPresetPicker, defaultActionPreset); + const defaultRemindMeBeforePicker = document.getElementById( + 'mailmindr-options_default-reminder-preset' + ); + createRemindMeBeforePicker( + document, + defaultRemindMeBeforePicker, + remindMeMinutesBefore, + defaultRemindMeMinutesBefore + ); + // // // @@ -201,7 +213,10 @@ const defaultIceBoxPicker = findIceboxPickerForIdentity('default'); if (defaultIceBoxPicker && defaultIceboxFolder) { - selectDefaultIceboxFolder(defaultIceBoxPicker, defaultIceboxFolder); + selectDefaultIceboxFolder( + defaultIceBoxPicker, + defaultIceboxFolder.folder + ); } }; @@ -228,8 +243,6 @@ const onMessage = async message => { const { topic = '' } = message; - logger.warn(`received topic: '${topic}'`); - switch (topic) { case 'settings:updated': { const { @@ -380,6 +393,15 @@ logger.log(`select default action preset:`, value); }); + const defaultRemindMeBeforePicker = document.getElementById( + 'mailmindr-options_default-reminder-preset' + ); + defaultRemindMeBeforePicker.addEventListener('change', async sender => { + const value = JSON.parse(sender.target.value); + logger.log(`select default remindMeMinutesBefore preset:`, value); + await set('defaultRemindMeMinutesBefore', value); + }); + // // // @@ -433,9 +455,9 @@ }); }; -document.addEventListener('DOMContentLoaded', async () => { - await onLoad(); -}); +// +// +// const createIceboxSettingLabel = (text, uniqueId) => { const label = document.createElement('label'); @@ -462,7 +484,10 @@ const value = JSON.parse(sender.target.value); const accountIdentity = sender.srcElement.dataset.accountIdentity; if (accountIdentity === 'default') { - await set('defaultIceboxFolder', { accountIdentity, value }); + await set('defaultIceboxFolder', { + accountIdentity, + folder: value + }); } else { await set('setIceBoxFolder', { accountIdentity, folder: value }); } @@ -479,3 +504,5 @@ return iceBoxPicker; }; + +onLoad(); diff -Nru mailmindr-1.4.0/views/popups/create-outgoing-mindr/index.css mailmindr-1.7.1/views/popups/create-outgoing-mindr/index.css --- mailmindr-1.4.0/views/popups/create-outgoing-mindr/index.css 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/views/popups/create-outgoing-mindr/index.css 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,173 @@ +@import url('../../mailmindr-core.css'); + +html { + padding: 0.5em; +} + +body.mailmindr-inline-popup main * { + font-size: 13px; +} + +body { + /* min-height: 320px; */ + /* min-width: 430px; */ + background-color: -moz-dialog; + /* -moz-default-background-color; */ +} + +h1 { + margin: 0 0 8px; + font-size: 22px; + font-weight: 300; + line-height: 1.3em; +} + +label { + user-select: none; +} + +input[type='checkbox'] { + /* display: none; */ + height: 16px; + width: 16px; +} + +input[type='checkbox'] + label { + padding: 0 0.5em; +} + +input[type='checkbox']:disabled + label { + color: silver; +} + +/* input[type='checkbox'] + label::before { + content: ''; + display: block; + float: left; + height: 16px; + width: 16px; + border: 1px solid rgba(12, 12, 13, 0.1); + border-radius: 2px; + background-color: var(--grey-90-a10); +} */ + +/* input[type='checkbox']:checked + label::before { + background-image: url(chrome://global/skin/icons/check.svg); + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAHklEQVQ4T2NkoBAwUqifYdQAhtEwACai0XQwGMIAACaYABGnE9aRAAAAAElFTkSuQmCC'); + background-position: center; + background-repeat: no-repeat; + background-color: blue; +} */ + +/* input[type='checkbox'] + label:hover::before { + background-color: var(--grey-90-a20); +} */ + +/* input[type='checkbox']:checked:hover + label::before { + background-color: var(--blue-70); +} */ + +input[type='date'], +input[type='time'] { + font-size: 100%; + padding: 4px 8px; + margin-bottom: 5px; + width: 100%; +} + +main { + display: flex; + flex-direction: column; +} + +.mailmindr-dialog { + display: grid; + /* grid-template-columns: 100px auto; */ + grid-template-columns: auto; + column-gap: 10px; + row-gap: 4px; + grid-auto-flow: row; +} + +.mailmindr-fieldwrapper--caption { + font-weight: bold; + min-height: 21px; +} + +.mailmindr-fieldwrapper--caption > label { + vertical-align: -webkit-baseline-middle; +} + +.mailmindr-fieldwrapper--content { + min-width: 0; +} + +.mailmindr-fieldwrapper--content > label { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: block; +} + +.mailmindr-buttonbar { + margin: 0.5em -0.25em; + color: initial; + /* grid-column: 2; */ + display: flex; + justify-content: flex-end; + /* width: 100%; */ + + /* bottom: 0; + position: fixed; + left: 0; + right: 0; */ +} + +.mailmindr-buttonbar--wrapper { + display: flex; + width: 100%; + flex-direction: column; +} + +button { + text-align: center; +} + +select { + color: inherit !important; + border: 1px solid transparent; + border-radius: 2px; + min-height: 30px; + font-weight: 400; + padding: 0 8px; + text-decoration: none; + font-size: 1em; + background-color: rgba(12, 12, 13, 0.1); + width: 100%; +} + +@media (prefers-color-scheme: dark) { + * { + color: #f9f9fa; + } + + body { + color: #f9f9fa; + background-color: #2a2a2e; + } + + input[type='text'], + input[type='number'], + select { + border-color: rgba(249, 249, 250, 0.2); + } + + input[type='date'], + input[type='time'] { + color: #f9f9fa; + background-color: #2a2a2e; + border-color: rgba(249, 249, 250, 0.2); + border-width: 1px; + border-radius: 2px; + } +} diff -Nru mailmindr-1.4.0/views/popups/create-outgoing-mindr/index.html mailmindr-1.7.1/views/popups/create-outgoing-mindr/index.html --- mailmindr-1.4.0/views/popups/create-outgoing-mindr/index.html 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/views/popups/create-outgoing-mindr/index.html 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,73 @@ + + + + + + + mailmindr + + +
+

+
+
+
+
+ +
+
+
+ +
+
+
+
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+ + + +
+
+ + + diff -Nru mailmindr-1.4.0/views/popups/create-outgoing-mindr/index.js mailmindr-1.7.1/views/popups/create-outgoing-mindr/index.js --- mailmindr-1.4.0/views/popups/create-outgoing-mindr/index.js 1970-01-01 00:00:00.000000000 +0000 +++ mailmindr-1.7.1/views/popups/create-outgoing-mindr/index.js 2024-08-04 20:14:20.000000000 +0000 @@ -0,0 +1,417 @@ +import { + genericFoldersAreEqual, + getGenericIceboxFolderForIdentityOrNull, + getParsedJsonOrDefaultValue, + localFolderToGenericFolder +} from '../../../modules/core-utils.mjs.js'; +import { + clearContents, + createRemindMeBeforePicker, + createTimePresetPicker, + isDarkMode, + selectDefaultActionPreset, + selectDefaultTimePreset +} from '../../../modules/ui-utils.mjs.js'; +import { + createCorrelationId, + createLogger +} from '../../../modules/logger.mjs.js'; +import { translateDocument } from '../../../modules/ui-utils.mjs.js'; + +const logger = createLogger('views/popups/create-outgoing-mindr'); + +const getDialogParameters = () => { + const correlationId = createCorrelationId(`getDialogParameters`); + logger.log('BEGIN getDialogParameters', { correlationId }); + const params = new URLSearchParams(window.location.search.substr(1)); + const result = { + dialogId: params.get('dialogId'), + headerMessageId: params.get('headerMessageId'), + author: params.get('author'), + subject: params.get('subject'), + folderAccountId: params.get('folderAccountId'), + folderName: params.get('folderName'), + folderPath: params.get('folderPath'), + folderType: params.get('folderType'), + guid: params.get('guid'), + folderAccountIdentityMailAddress: params.get( + 'folderAccountIdentityMailAddress' + ) + }; + + logger.log('dialog result', { correlationId, result }); + logger.log('END getDialogParameters', { correlationId }); + + return result; +}; + +const closePopup = async () => { + const correlationId = createCorrelationId('closePopup'); + logger.log(`BEGIN closePopup`, { correlationId }); + logger.log(`try to get dialog parameters`); + try { + const { dialogId } = getDialogParameters(); + logger.log(`closing dialog ${dialogId}`); + window.close(); + } catch (e) { + logger.error('create-outgoing-mindr::closePopup', e); + } + logger.log('END closePopup', { correlationId }); +}; + +const doCancel = async () => await closePopup(); + +const resetComposeActionIconAndLabel = async id => { + const path = isDarkMode() + ? '/images/mailmindr-flag--white.svg' + : '/images/mailmindr-flag.svg'; + await Promise.all([ + messenger.composeAction.setIcon({ + path, + tabId: id + }), + messenger.composeAction.setLabel({ + label: messenger.i18n.getMessage('mailmindrComposeMessageButton'), + tabId: id + }) + ]); +}; + +const handleChangeTimePreset = event => { + const { + target: { value } + } = event; + + const preset = getParsedJsonOrDefaultValue(value, {}); + const { days, hours, minutes, isRelative, isSelectable } = preset; + + if (!isSelectable) { + logger.log('nothing to change here'); + return; + } + + const millisecondsForDays = days * 24 * 60 * 60 * 1000; + let now = Date.now() + millisecondsForDays; + + if (isRelative) { + now += + hours * 60 * 60 * 1000 /* add hours */ + + minutes * 60 * 1000; /* add minutes */ + } + + const nowAsDate = new Date(now); + const newDate = new Date( + Date.UTC( + nowAsDate.getFullYear(), + nowAsDate.getMonth(), + nowAsDate.getDate() + ) + ); + /** @type {HTMLInputDateElement} */ + const datePicker = document.getElementById('mailmindr--date-picker'); + /** @type {HTMLInputTimeElement} */ + const timePicker = document.getElementById('mailmindr--time-picker'); + + datePicker.valueAsDate = newDate; + + if (isRelative) { + const hourString = `0${nowAsDate.getHours()}`.substr(-2); + const minuteString = `0${nowAsDate.getMinutes()}`.substr(-2); + const newTimePickerValue = `${hourString}:${minuteString}`; + + timePicker.value = newTimePickerValue; + } else { + const hourString = `0${hours}`.substr(-2); + const minuteString = `0${minutes}`.substr(-2); + const newTimePickerValue = `${hourString}:${minuteString}`; + + timePicker.value = newTimePickerValue; + + const pickedDateTime = getDateTimefromPickers(); + + logger.info(pickedDateTime, new Date()); + + if (pickedDateTime <= new Date()) { + logger.info( + 'adjusting time by adding a day, adjusted to: ', + getDateTimefromPickers() + ); + // + datePicker.valueAsDate = new Date(Date.now() + 24 * 60 * 60 * 1000); + } + } +}; + +const getDateTimefromPickers = () => { + /** @type {HTMLInputDateElement} */ + const datePicker = document.getElementById('mailmindr--date-picker'); + /** @type {HTMLInputTimeElement} */ + const timePicker = document.getElementById('mailmindr--time-picker'); + + const timePickerValue = timePicker.value; + const [hours, minutes] = timePickerValue + .split(':') + .map(item => Number.parseInt(item, 10)); + + const [year, month, day] = datePicker.value.split('-'); + const newDate = new Date(year, month - 1, day, hours, minutes, 0, 0); + + logger.warn( + 'current date from datepicker without adjusting time and timezone', + newDate + ); + + return newDate; +}; + +const getActionTemplateFromPicker = () => { + const actionTemplatePicker = /** @type {HTMLInputElement} */ (document.getElementById( + 'mailmindr--preset-action' + )); + const actionTemplate = JSON.parse(actionTemplatePicker.value); + + return actionTemplate; +}; + +const doRemove = async () => { + const { windowId, id } = await messenger.tabs.getCurrent(); + const sender = { windowId, id }; + const removed = await messenger.runtime.sendMessage({ + action: 'mindr:remove-outgoing-mindr', + payload: { sender } + }); + + if (removed && removed.status && removed.status === 'ok') { + await resetComposeActionIconAndLabel(id); + doCancel(); + } +}; + +const onLoad = async () => { + const { windowId, id } = await messenger.tabs.getCurrent(); + const result = await messenger.runtime.sendMessage({ + action: 'dialog:open', + payload: { name: 'set-outgoing-mindr', sender: { windowId, id } } + }); + + const { + payload: { + settings, + presets: { time, actions, remindMeMinutesBefore }, + draft + } + } = result; + + const editMindr = Boolean(draft); + const removeButton = document.getElementById('mailmindr--do-remove-mindr'); + removeButton.addEventListener('click', doRemove); + removeButton.setAttribute( + 'style', + editMindr ? 'display: block;' : 'display: none;' + ); + + const cancelButton = document.getElementById( + 'mailmindr--do-cancel-create-mindr' + ); + cancelButton.addEventListener('click', doCancel); + + const acceptButton = document.getElementById('mailmindr--do-create-mindr'); + acceptButton.addEventListener('click', async ( + source, + /** @type{ MouseEvent} */ event + ) => { + const sender = await messenger.windows.getLastFocused({ + windowTypes: ['messageCompose'] + }); + doAccept(sender); + }); + + /** @type {HTMLInputDateElement} */ + const datePicker = document.getElementById('mailmindr--date-picker'); + /** @type {HTMLInputTimeElement} */ + const timePicker = document.getElementById('mailmindr--time-picker'); + + const now = new Date(); + const hours = `0${now.getHours()}`.substr(-2); + const minutes = `0${now.getMinutes()}`.substr(-2); + + timePicker.value = [hours, minutes].join(':'); + datePicker.valueAsDate = now; + + const actionPresets = document.getElementById('mailmindr--preset-action'); + actions.map(actionPreset => { + const actionOption = document.createElement('option'); + actionOption.value = JSON.stringify(actionPreset); + actionOption.innerText = actionPreset.text; + + actionPresets.appendChild(actionOption); + }); + + const timePresets = createTimePresetPicker( + document.getElementById('mailmindr--preset-time'), + time + ); + const handlePickerChange = () => { + const presetTimePicker = /** @type {HTMLSelectElement} */ (document.getElementById( + 'mailmindr--preset-time' + )); + presetTimePicker.selectedIndex = 0; + }; + + datePicker.addEventListener('change', handlePickerChange); + timePicker.addEventListener('change', handlePickerChange); + timePresets.addEventListener('change', handleChangeTimePreset); + + if (editMindr) { + const { mindr } = draft; + const due = mindr.due; + const dueHours = `0${due.getHours()}`.substr(-2); + const dueMinutes = `0${due.getMinutes()}`.substr(-2); + + timePicker.value = [dueHours, dueMinutes].join(':'); + datePicker.valueAsDate = due; + + timePresets.selectedIndex = 0; + + const preselectedAction = mindr?.actionTemplate + ? { ...mindr?.actionTemplate, moveMessageTo: undefined } + : undefined; + + selectDefaultActionPreset(actionPresets, preselectedAction); + } else { + const defaultTimePreset = settings?.defaultTimePreset; + const defaultActionPreset = settings?.defaultActionPreset; + + selectDefaultTimePreset(timePresets, defaultTimePreset); + selectDefaultActionPreset(actionPresets, defaultActionPreset); + + const changeEvent = new Event('change'); + timePresets.dispatchEvent(changeEvent); + } + + const remindMe = document.getElementById('mailmindr--preset-remind-me'); + // + createRemindMeBeforePicker( + document, + remindMe, + remindMeMinutesBefore, + settings.defaultRemindMeMinutesBefore + ); + + translateDocument(document); +}; + +document.addEventListener('DOMContentLoaded', onLoad, { once: true }); +document.addEventListener('keydown', async event => { + if (event.code === 'Escape') { + await doCancel(); + } +}); + +const doAccept = async (/** @type {Window} */ sender) => { + // + const remindMe = document.getElementById('mailmindr--preset-remind-me'); + const remindMeMinutesBefore = parseInt( + /** @type {HTMLInputElement} */ (remindMe).value || '0', + 10 + ); + + const due = getDateTimefromPickers(); + const actionTemplate = getActionTemplateFromPicker(); + + // + // + // + // + // + // + // + // + // + + // + + const { + guid, + headerMessageId, + author, + subject, + folderAccountId, + folderName, + folderPath, + folderType, + folderAccountIdentityMailAddress + } = getDialogParameters(); + + if (remindMeMinutesBefore) { + if (remindMeMinutesBefore >= 0) { + actionTemplate.showReminder = true; + } else { + actionTemplate.showReminder = false; + } + } + + const { windowId, id } = await messenger.tabs.getCurrent(); + const payload = { + mindr: { + guid, + headerMessageId, + due, + remindMeMinutesBefore, + actionTemplate, + notes: '', + metaData: { + headerMessageId, + author, + subject, + folderAccountId, + folderName, + folderPath, + folderType, + folderAccountIdentityMailAddress + }, + isWaitingForReply: true, + doMoveToIcebox: false + }, + sender: { windowId, id } + }; + const result = await messenger.runtime.sendMessage({ + action: 'mindr:create-outgoing', + payload + }); + if (result && result.status === 'ok') { + // + const path = isDarkMode() + ? '/images/mailmindr-flag_marker--white.svg' + : '/images/mailmindr-flag_marker.svg'; + const currentLocale = navigator.language; + const formattedTime = new Intl.DateTimeFormat(currentLocale, { + timeStyle: 'short' + }).format(due); + const formattedDate = new Intl.DateTimeFormat(currentLocale, { + dateStyle: 'short' + }).format(due); + const formattedDateAndTime = new Intl.DateTimeFormat(currentLocale, { + dateStyle: 'medium', + timeStyle: 'short' + }).format(due); + + await Promise.all([ + messenger.composeAction.setIcon({ + path, + tabId: id + }), + messenger.composeAction.setLabel({ + label: messenger.i18n.getMessage( + 'mailmindrComposeMessageButton.detailed', + [formattedDate, formattedTime, formattedDateAndTime] + ), + tabId: id + }) + ]); + + doCancel(); + } else { + await resetComposeActionIconAndLabel(id); + } +}; diff -Nru mailmindr-1.4.0/views/popups/list-all/index.css mailmindr-1.7.1/views/popups/list-all/index.css --- mailmindr-1.4.0/views/popups/list-all/index.css 2022-08-12 13:32:09.000000000 +0000 +++ mailmindr-1.7.1/views/popups/list-all/index.css 2024-08-04 20:14:20.000000000 +0000 @@ -23,15 +23,28 @@ text-align: center; } +button.mailmindr-button--micro { + font-size: 12px; + height: unset; + min-height: 21px; +} + button:hover { border-color: transparent; } .mailmindr-popup-header { border-bottom: 0.5px solid silver; - padding: 0.75em 1em; + /* padding: 0.75em 1em; */ + padding: 0.25em 0.5em; text-align: center; - background: linear-gradient(45deg, var(--purple-40), var(--purple-50)); + /* background: linear-gradient(45deg, var(--purple-40), var(--purple-50)); */ + background-color: #eee; +} + +.mailmindr-popup-header button { + margin: 0.25em; + min-height: 23px; } .mailmindr-popup-header a { @@ -195,11 +208,53 @@ color: var(--grey-90); } +.mailmindr-list-label-wrapper { + padding-left: 12px; + margin-bottom: 2px; +} + +.mailmindr-list-label { + border-radius: 4px; + color: var(--grey-90); + text-transform: uppercase; + padding: 2px 6px; + margin: 2px; + font-size: smaller; + background-color: var(--yellow-50); + display: inline; +} + +.mailmindr-list-label--awaiting::before { + content: ''; + height: 12px; + width: 12px; + background-image: url(chrome://messenger/skin/icons/hourglass.svg); + background-repeat: no-repeat; + background-position: center center; + padding: 10px; +} +.mailmindr-list-label--awaiting-reply-received { + text-decoration: line-through; +} + @media (prefers-color-scheme: dark) { + * { + color: #f9f9fa; + } + + body { + color: #f9f9fa; + background-color: #2a2a2e; + } + footer { background-color: var(--grey-90-a80); } + .mailmindr-popup-header { + background-color: var(--grey-90-a80); + } + .mailmindr-list-placeholder p { color: var(--grey-10); } diff -Nru mailmindr-1.4.0/views/popups/list-all/index.html mailmindr-1.7.1/views/popups/list-all/index.html --- mailmindr-1.4.0/views/popups/list-all/index.html 2022-08-12 13:32:08.000000000 +0000 +++ mailmindr-1.7.1/views/popups/list-all/index.html 2024-08-04 20:14:20.000000000 +0000 @@ -34,11 +34,30 @@ -->
+ + +
    diff -Nru mailmindr-1.4.0/views/popups/list-all/index.js mailmindr-1.7.1/views/popups/list-all/index.js --- mailmindr-1.4.0/views/popups/list-all/index.js 2022-08-12 13:32:08.000000000 +0000 +++ mailmindr-1.7.1/views/popups/list-all/index.js 2024-08-04 20:14:20.000000000 +0000 @@ -5,6 +5,7 @@ import { createLogger } from '../../../modules/logger.mjs.js'; import { webHandler } from '../../../modules/ui-utils.mjs.js'; import { translateDocument } from '../../../modules/ui-utils.mjs.js'; +import { hasReply } from '../../../modules/core-utils.mjs.js'; let currentMindrGuid = null; @@ -38,11 +39,18 @@ window.close(); }; +/** + * Render item markup for a mindr + * + * @param {Mindr} mindr + * @returns HTMLElement + */ const createMindrItem = mindr => { const { guid, due, - metaData: { author, subject } + metaData: { author, subject }, + isWaitingForReply = false } = mindr; const relative = due - Date.now(); const item = document.createElement('li'); @@ -111,6 +119,28 @@ contentWrapper.appendChild(authorWrapper); contentWrapper.appendChild(dueWrapper); + if (isWaitingForReply) { + const markerWrapper = document.createElement('div'); + const markerItem = document.createElement('div'); + + markerWrapper.className = 'mailmindr-list-label-wrapper'; + + markerItem.classList.add('mailmindr-list-label'); + markerItem.classList.add('mailmindr-list-label--awaiting'); + + if (hasReply(mindr)) { + markerItem.classList.add( + 'mailmindr-list-label--awaiting-reply-received' + ); + } + + const markerLabel = document.createTextNode('awaiting reply'); + + markerItem.appendChild(markerLabel); + markerWrapper.appendChild(markerItem); + contentWrapper.appendChild(markerWrapper); + } + item.appendChild(contentWrapper); item.addEventListener('click', async () => openMessageByGuid(guid)); item.addEventListener('mouseover', async () => { @@ -155,26 +185,16 @@ const selectNextElement = (listElement, guid, direction) => { const children = Array.from(listElement.children); if (guid) { - /** @type {HTMLLIElement} */ - const selectedElement = document.querySelector( - `li[data-guid='${guid}'` - ); toggleItemSelection(guid, false); - let nextElement = selectedElement; const index = children.findIndex( element => element.dataset.guid === guid ); - if (direction !== 0) { - if (index === children.length - 1) { - nextElement = children[0]; - } else if (index === 0 && direction < 0) { - nextElement = children[children.length - 1]; - } else { - nextElement = children[index + direction]; - } - } + const newIndex = + (index + children.length + direction) % children.length; + const nextElement = children[newIndex]; + currentMindrGuid = nextElement.dataset.guid; toggleItemSelection(currentMindrGuid, true); } else { @@ -210,6 +230,7 @@ if (mindrs.length) { clearList(listElement); displayList(listElement, mindrs); + selectNextElement(listElement, mindrs[0].guid, 0); // /** @type {HTMLElement} */ @@ -233,6 +254,45 @@ // } + const searchField = /** @type {HTMLInputElement} */ document.getElementById( + 'mailmindr--search-field' + ); + searchField.addEventListener('keydown', event => { + const { code, shiftKey, metaKey, altKey, ctrlKey } = event; + + const text = searchField.value; + let items = mindrs; + if (code === 'Delete' && shiftKey && !metaKey && !altKey && !ctrlKey) { + // + searchField.value = ''; + } else if (text.length >= 3) { + items = mindrs.filter(item => { + const { + metaData: { author, subject } + } = item; + return ( + (author || '').toLowerCase().indexOf(text) >= 0 || + (subject || '').toLowerCase().indexOf(text) >= 0 + ); + }); + } + + clearList(listElement); + displayList(listElement, items); + if (items && items.length) { + selectNextElement(listElement, items[0].guid, 0); + } + }); + + const clearFilterButton = document.getElementById( + 'mailmindr--search-field-clear' + ); + clearFilterButton.addEventListener('click', () => { + searchField.value = ''; + clearList(listElement); + displayList(listElement, mindrs); + }); + Array.from( document.querySelectorAll('button.mailmindr-button-web') ).forEach(btn => {