diff --git a/CHANGELOG.md b/CHANGELOG.md index 150fa7ea..5fc61e81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@
  • Added the {%mail_full_headers%} placeholder to retrieve all the email headers at once [#713].
  • [All APIs] In the API webchat the status messages have different colors [#3].
  • Account exclusion lists for add tags and antispam are enforced only for automatic analysis of incoming emails and not for the context menu action that is always executed [#749].
  • +
  • [All APIs] Inline auto translation for emails added [#247].
  • +
  • Custom menus configuration added. Now it's possibile to define which prompts show in the ThunderAI menu, which ones in the context menu and in which order [#49, #184, #680].
  • ...
  • Version 4.0.3 - 20/03/2026

    diff --git a/README.md b/README.md index f52bf24a..ba125488 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,8 @@ _The language status represents the percentage of translated strings in the late - [Roundicons](https://www.flaticon.com/authors/roundicons) for the summarize context menu icon - [HideMau](https://www.flaticon.com/authors/hidemaru) for the ai summarize icon - [Hilmy Abiyyu A.](https://www.flaticon.com/authors/hilmy-abiyyu-a) for the ai translate and context menu icons +- [bearicons](https://www.flaticon.com/authors/bearicons) for the empty context menu icon +- [meaicon](https://www.flaticon.com/authors/meaicon) for the add task context menu icon
    diff --git a/_locales/en/messages.json b/_locales/en/messages.json index b94db7f7..cb1b54d3 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -35,12 +35,8 @@ "message": "Classify", "description": "" }, - "prompt_summarize_this": { - "message": "Summarize this", - "description": "" - }, "prompt_translate_this": { - "message": "Translate this", + "message": "Translate", "description": "" }, "prompt_this": { @@ -203,6 +199,22 @@ "message": "Composing an email", "description": "" }, + "show_in": { + "message": "Show in", + "description": "" + }, + "show_in_popup": { + "message": "Popup only", + "description": "" + }, + "show_in_context": { + "message": "Context menu only", + "description": "" + }, + "show_in_both": { + "message": "Both", + "description": "" + }, "customPrompts_close_button": { "message": "Close button", "description": "" @@ -612,14 +624,6 @@ "message": "If checked, using the keyboard shortcut CTRL+ALT+A will automatically send the highlighted prompt from the menu. Otherwise, the prompt name will be displayed to the user, requiring another press of the Enter key to send it.", "description": "" }, - "prefs_OptionText_dynamic_menu_order_alphabet": { - "message": "Menu: order alphabetically", - "description": "" - }, - "prefs_OptionText_dynamic_menu_order_alphabet_info": { - "message": "If checked, the prompts in the menu will be ordered alphabetically.", - "description": "" - }, "prefs_OptionText_chatgpt_win_dims_info": { "message": "Set to 0 if you don't want to specify the window size.", "description": "" @@ -816,10 +820,6 @@ "message": "Classify the following text in terms of Politeness, Warmth, Formality, Assertiveness, Offensiveness giving a percentage for each category. Reply with only the category and score with no extra comments or other text.", "description": "" }, - "prompt_summarize_this_full_text": { - "message": "Summarize the following email into a bullet point list.", - "description": "" - }, "prompt_translate_this_full_text": { "message": "Translate the email below into {%thunderai_translate_lang%}.\n\nRules:\n- Translate both the subject and the body.\n- Return the result as a JSON object with three fields: \"subject\", \"body\" and \"status\".\n- If the translation has been done the status is equal to 1.\n- If the email is written in one of these languages \"{%thunderai_translate_exclude_lang%}\" or in the {%thunderai_translate_lang%} language, return an empty string for the body and the subject and set the status to -1.\n- Do not add explanations, notes, or any text outside the JSON.\n\nMail subject: {%mail_subject%}\n\nMail body: {%mail_html_body%}\n\nGenerate a response in JSON format only. The output should be only a JSON object. Here is an example of the JSON format to be used:\n{\n\"subject\": \"subject translation\",\n\"body\": \"body translation\",\n\"status\": \"status result\"\n}", "description": "" @@ -837,7 +837,7 @@ "description": "" }, "prompt_add_tags": { - "message": "Add tags to this email", + "message": "Add tags", "description": "" }, "prompt_add_tags_full_text": { @@ -1105,7 +1105,7 @@ "description": "" }, "prompt_summarize": { - "message": "Summarize this email or these emails", + "message": "Summarize", "description": "" }, "prompt_summarize_full_text": { @@ -1377,7 +1377,7 @@ "description": "" }, "prompt_spamfilter" : { - "message" : "Detect spam emails", + "message" : "Analyze for spam", "description" : "" }, "prompt_spamfilter_full_text" : { @@ -1520,22 +1520,6 @@ "message": "No", "description": "" }, - "context_menu_mzta-add-tags": { - "message": "Add tags", - "description": "" - }, - "context_menu_mzta-spamfilter": { - "message": "Analyze for spam", - "description": "" - }, - "context_menu_mzta-summarize": { - "message": "Summarize", - "description": "" - }, - "context_menu_mzta-translate": { - "message": "Translate", - "description": "" - }, "noActiveCalendar": { "message": "No editable calendar found!", "description": "" @@ -2306,5 +2290,77 @@ "placeholder_string": { "message": "Placeholder", "description": "" + }, + "menu_order_title": { + "message": "Menu Order", + "description": "Title for the menu order page" + }, + "menu_order_popup_list_title": { + "message": "Popup Menu", + "description": "Header for the popup menu list in the menu order page" + }, + "menu_order_context_list_title": { + "message": "Context Menu", + "description": "Header for the context menu list in the menu order page" + }, + "menu_order_saved": { + "message": "Menu order saved!", + "description": "Message shown after saving menu order" + }, + "menu_order_tab_reading": { + "message": "Reading", + "description": "Sub-tab label for reading view ordering" + }, + "menu_order_tab_composing": { + "message": "Composing", + "description": "Sub-tab label for composing view ordering" + }, + "menu_order_badge_default": { + "message": "Default", + "description": "Badge label for default prompts" + }, + "menu_order_badge_special": { + "message": "Special", + "description": "Badge label for special prompts" + }, + "menu_order_badge_custom": { + "message": "Custom", + "description": "Badge label for custom prompts" + }, + "menu_order_type_reading": { + "message": "Reading", + "description": "Badge label for reading-only prompts" + }, + "menu_order_type_composing": { + "message": "Composing", + "description": "Badge label for composing-only prompts" + }, + "menu_order_type_always": { + "message": "Always", + "description": "Badge label for prompts shown in all contexts" + }, + "menu_order_btn_label": { + "message": "Manage menu order settings", + "description": "Button label to open the menu order page from options" + }, + "menu_order_info": { + "message": "Drag and drop items to reorder them. Use the toggle to show or hide items in each menu.", + "description": "Info text for the menu order page" + }, + "menu_order_active_items": { + "message": "Visible items", + "description": "Section header for active/visible menu items" + }, + "menu_order_hidden_items": { + "message": "Hidden items", + "description": "Section header for hidden menu items" + }, + "menu_order_icon_label": { + "message": "Choose an icon", + "description": "Label/tooltip for the context menu icon picker on the menu order page" + }, + "menu_order_icon_none": { + "message": "(none)", + "description": "Dropdown option meaning no custom icon is selected for the context menu item" } } \ No newline at end of file diff --git a/claude-spec/01-architecture.md b/claude-spec/01-architecture.md index 8682a03e..082f7b8c 100644 --- a/claude-spec/01-architecture.md +++ b/claude-spec/01-architecture.md @@ -210,6 +210,7 @@ Each subdirectory is a self-contained settings/UI page for a specific feature: | `customdataplaceholders/` | Custom placeholder editor | | `get-calendar-event/` | Calendar event extraction settings | | `get-task/` | Task creation settings | +| `menu_order/` | Drag-and-drop reordering and visibility control for popup and context menus | | `spamfilter/` | Spam filter settings | | `summarize/` | Email summarization settings | | `translate/` | Email translation settings | diff --git a/claude-spec/02-prompts.md b/claude-spec/02-prompts.md index 60b1624f..2745ac3d 100644 --- a/claude-spec/02-prompts.md +++ b/claude-spec/02-prompts.md @@ -28,9 +28,12 @@ Prompts are the core user-facing feature of ThunderAI. Each prompt defines an AI | Property | Type | Description | |----------|------|-------------| -| `enabled` | number | `0` = hidden, `1` = shown in popup | -| `position_display` | number | Sort order in reading view | -| `position_compose` | number | Sort order in compose view | +| `enabled` | number | `0` = hidden, `1` = shown in menus | +| `position_display` | number | Sort order for the popup menu in reading view | +| `position_compose` | number | Sort order for the popup menu in compose view | +| `position_context` | number | Sort order for the context menu | +| `show_in` | string | `"popup"` = popup only, `"context"` = context menu only, `"both"` = both, `"none"` = hidden from all menus. Default: `"popup"` for default/custom prompts, `"both"` for special prompts | +| `custom_icon` | string | Filename (with extension) of an icon in `images/context_menu/custom/` used as the context-menu icon. Empty string = no icon. Only used for non-special prompts (special prompts use their hard-coded icons in `specialPromptToContextMenuID`). Selectable from a dropdown on the Menu Order page, context-menu tab. | ### Per-Prompt API Override Properties @@ -59,6 +62,70 @@ Some prompts trigger additional Thunderbird actions beyond just sending text to These special prompts can have their own dedicated API integration settings (configured in the Options page). The list of these special prompts is in `options/mzta-options-default.js` as `special_prompts_with_integration`. +## Menu System + +### Popup Menu +- Displays prompts filtered by `show_in` (`"popup"` or `"both"`) and by tab context (`type` property: reading view shows types `0`+`1`, compose view shows types `0`+`2`) +- Ordering: always position-based using `position_display` (reading view) or `position_compose` (compose view). Alphabetical ordering has been removed +- Special prompts retain their colored background (CSS class `special_prompt`) in the popup based on `is_special == "1"` + +### Context Menu +- Dynamically built from all prompts with `show_in` set to `"context"` or `"both"`, filtered to reading types only (`type` 0 or 1) +- Appears as a "ThunderAI" submenu in the `message_list` context +- Ordering: position-based using `position_context` (fallback to alphabetical only when positions are equal) +- Special prompts (add_tags, spamfilter, summarize, translate) route through `processEmails()` for batch processing; regular prompts execute via `menus.executeMenuAction()` +- Icons: special prompts use dedicated icons (defined in `contextMenuIconsPath`); all other prompts use the addon icon (`images/icon-32.png`) +- Add Tags in context menu assigns tags automatically (`addTagsAuto: true`), while in the popup it shows the interactive tag selection form + +### Menu Order Page (`pages/menu_order/`) + +Dedicated page for reordering, enabling, and disabling menu items across both the popup and the context menu. Opened from the options page via the "Menu Order" button. + +**UI layout** — two side-by-side panels: +- **Popup Menu panel**: sub-tabs for "Reading" / "Composing" switch the list between `position_display` / `position_compose` ordering and between the allowed types (`0`+`1` vs `0`+`2`) +- **Context Menu panel**: single list ordered by `position_context`. Items with `type: "2"` (composing-only) are never shown here + +Each list has two sections: +- **Visible items**: active for the menu (`show_in` includes the menu), draggable to reorder +- **Hidden items**: inactive for the menu (`show_in` excludes the menu), sorted alphabetically, not draggable + +**Toggle coordination** — flipping the checkbox updates the prompt's `show_in` with four-state logic: +- Popup ON: `"none"` → `"popup"`, `"context"` → `"both"` +- Popup OFF: `"popup"` → `"none"`, `"both"` → `"context"` +- Context ON: `"none"` → `"context"`, `"popup"` → `"both"` +- Context OFF: `"context"` → `"none"`, `"both"` → `"popup"` + +**Drag and drop** — native HTML5 DnD assigns sequential position numbers (1, 2, 3, ...) to `position_display`, `position_compose`, or `position_context` depending on which list is being sorted. + +**Exclusions from the UI** (preserved on save so data is not lost): +- Prompts with `enabled === 0` (disabled) +- Special prompts whose base definition has `show_in: "none"` (internal prompts like `prompt_summarize_email_template` and `prompt_summarize_email_separator`) — retrieved via `getHiddenSpecialPromptIds()` +- Special prompts whose feature is not active — retrieved from background via `get_active_special_ids` message, which calls `getActiveSpecialPromptsIDs()` with current prefs and `_sparks_presence` + +**Cross-tab reload** — the page listens on `browser.storage.onChanged` for changes to `_default_prompts_properties`, `_custom_prompt`, or `_special_prompts`. When one of those keys changes (e.g. user saves from the Custom Prompts page in another tab), the page reloads its data with a 200ms debounce. Any unsaved local changes are discarded to avoid overwriting the other page's work. + +**Save flow**: +1. Re-concat preserved prompts (disabled + hidden-specials + inactive-feature specials) with the UI-visible prompts +2. Split by `is_default` / `is_special` and call `setDefaultPromptsProperties()`, `setCustomPrompts()`, `setSpecialPrompts()` +3. Send `reload_menus` to the background to rebuild both menus + +### Alphabetic-to-Position Migration + +The `dynamic_menu_order_alphabet` preference (previously a user-facing option) has been retired and removed from the UI, but the key still exists in storage as a one-shot migration flag. At every background startup, `migrateMenuOrderAlphabetic()` in `js/mzta-prompts.js` runs: + +1. Reads `dynamic_menu_order_alphabet` (defaults to `true` if unset) +2. If `true`: sorts all visible prompts with special prompts first (alphabetically), then the rest (alphabetically), and assigns sequential `position_display` = `position_compose` = `position_context` numbers. Hidden special prompts are preserved untouched. +3. Persists the new positions via `setDefaultPromptsProperties` / `setCustomPrompts` / `setSpecialPrompts` +4. Sets `dynamic_menu_order_alphabet = false` in sync storage so the migration does not run again + +This ensures existing users upgrading from the previous alphabetical-default behaviour get the same visible ordering on first run, while subsequent launches keep whatever custom ordering the user has set. + +### Special Prompt Visibility Dependencies + +`getActiveSpecialPromptsIDs()` in `js/mzta-utils.js` maps feature prefs to active special prompt IDs. Notable dependency: + +- `prompt_get_calendar_event_from_clipboard` is emitted only if **both** `get_calendar_event` and `get_calendar_event_from_clipboard` are active. If `get_calendar_event` is off, neither calendar prompt is shown regardless of the clipboard pref. + ### Summarize: Dual-Mode Prompt System The summarize feature uses two distinct prompt pathways: diff --git a/claude-spec/05-options.md b/claude-spec/05-options.md index 0c968c88..32e1256b 100644 --- a/claude-spec/05-options.md +++ b/claude-spec/05-options.md @@ -64,7 +64,7 @@ These are generated programmatically at the bottom of `mzta-options-default.js` | `chatgpt_web_custom_gpt` | `''` | Custom GPT URL | | `chatgpt_web_load_wait_time` | `1000` | Wait time (ms) for ChatGPT page | | `dynamic_menu_force_enter` | `false` | Force Enter to submit in popup | -| `dynamic_menu_order_alphabet` | `true` | Sort prompts alphabetically | +| `dynamic_menu_order_alphabet` | `true` | Internal migration flag only; no UI. Set to `false` by `migrateMenuOrderAlphabetic()` on first boot after upgrade to bootstrap position-based ordering. See `claude-spec/02-prompts.md` for details. | | `placeholders_use_default_value` | `false` | Use placeholder defaults when empty | | `max_prompt_length` | `30000` | Max prompt string length | @@ -126,6 +126,10 @@ The summarize settings page provides: - Each has Save/Reset buttons and placeholder autocomplete - Default text comes from i18n strings (`prompt_summarize_full_text`, etc.) +### Menu Order Page (`pages/menu_order/`) + +Entry point from the options page via the "Menu Order" button (next to "Manage your prompts"). Provides drag-and-drop reordering and toggle-based visibility control for both the popup and the context menu. See `claude-spec/02-prompts.md` ("Menu Order Page") for the full behaviour, data flow, and exclusion rules. + ### Translate Settings Page (`pages/translate/`) The translate settings page provides: diff --git a/images/menu_autotags.png b/images/context_menu/autotags.png similarity index 100% rename from images/menu_autotags.png rename to images/context_menu/autotags.png diff --git a/images/context_menu/custom/adventure-game.png b/images/context_menu/custom/adventure-game.png new file mode 100644 index 00000000..4887f773 Binary files /dev/null and b/images/context_menu/custom/adventure-game.png differ diff --git a/images/context_menu/custom/airport.png b/images/context_menu/custom/airport.png new file mode 100644 index 00000000..9fb2dc7f Binary files /dev/null and b/images/context_menu/custom/airport.png differ diff --git a/images/context_menu/custom/alert-sign.png b/images/context_menu/custom/alert-sign.png new file mode 100644 index 00000000..e1a0430e Binary files /dev/null and b/images/context_menu/custom/alert-sign.png differ diff --git a/images/context_menu/custom/calendar.png b/images/context_menu/custom/calendar.png new file mode 100644 index 00000000..34842b22 Binary files /dev/null and b/images/context_menu/custom/calendar.png differ diff --git a/images/context_menu/custom/cash.png b/images/context_menu/custom/cash.png new file mode 100644 index 00000000..6e13026a Binary files /dev/null and b/images/context_menu/custom/cash.png differ diff --git a/images/context_menu/custom/clapboard.png b/images/context_menu/custom/clapboard.png new file mode 100644 index 00000000..c07e6aab Binary files /dev/null and b/images/context_menu/custom/clapboard.png differ diff --git a/images/context_menu/custom/clock.png b/images/context_menu/custom/clock.png new file mode 100644 index 00000000..1450641a Binary files /dev/null and b/images/context_menu/custom/clock.png differ diff --git a/images/context_menu/custom/coins.png b/images/context_menu/custom/coins.png new file mode 100644 index 00000000..6be90430 Binary files /dev/null and b/images/context_menu/custom/coins.png differ diff --git a/images/context_menu/custom/copywriting.png b/images/context_menu/custom/copywriting.png new file mode 100644 index 00000000..6ba918fa Binary files /dev/null and b/images/context_menu/custom/copywriting.png differ diff --git a/images/context_menu/custom/customer-service.png b/images/context_menu/custom/customer-service.png new file mode 100644 index 00000000..8013181b Binary files /dev/null and b/images/context_menu/custom/customer-service.png differ diff --git a/images/context_menu/custom/deadline.png b/images/context_menu/custom/deadline.png new file mode 100644 index 00000000..37041192 Binary files /dev/null and b/images/context_menu/custom/deadline.png differ diff --git a/images/context_menu/custom/discount.png b/images/context_menu/custom/discount.png new file mode 100644 index 00000000..9fa4cf23 Binary files /dev/null and b/images/context_menu/custom/discount.png differ diff --git a/images/context_menu/custom/earth.png b/images/context_menu/custom/earth.png new file mode 100644 index 00000000..a7860207 Binary files /dev/null and b/images/context_menu/custom/earth.png differ diff --git a/images/context_menu/custom/electric-car.png b/images/context_menu/custom/electric-car.png new file mode 100644 index 00000000..7ce1265f Binary files /dev/null and b/images/context_menu/custom/electric-car.png differ diff --git a/images/context_menu/custom/empty_icon.png b/images/context_menu/custom/empty_icon.png new file mode 100644 index 00000000..e7ebda09 Binary files /dev/null and b/images/context_menu/custom/empty_icon.png differ diff --git a/images/context_menu/custom/experiment.png b/images/context_menu/custom/experiment.png new file mode 100644 index 00000000..fb3ae625 Binary files /dev/null and b/images/context_menu/custom/experiment.png differ diff --git a/images/context_menu/custom/express-delivery.png b/images/context_menu/custom/express-delivery.png new file mode 100644 index 00000000..d115986a Binary files /dev/null and b/images/context_menu/custom/express-delivery.png differ diff --git a/images/context_menu/custom/flammable.png b/images/context_menu/custom/flammable.png new file mode 100644 index 00000000..cc7aeb0f Binary files /dev/null and b/images/context_menu/custom/flammable.png differ diff --git a/images/context_menu/custom/home.png b/images/context_menu/custom/home.png new file mode 100644 index 00000000..bf6a2bb3 Binary files /dev/null and b/images/context_menu/custom/home.png differ diff --git a/images/context_menu/custom/idea.png b/images/context_menu/custom/idea.png new file mode 100644 index 00000000..84a43e13 Binary files /dev/null and b/images/context_menu/custom/idea.png differ diff --git a/images/context_menu/custom/info.png b/images/context_menu/custom/info.png new file mode 100644 index 00000000..6471faf7 Binary files /dev/null and b/images/context_menu/custom/info.png differ diff --git a/images/context_menu/custom/invoice.png b/images/context_menu/custom/invoice.png new file mode 100644 index 00000000..01ecd7c9 Binary files /dev/null and b/images/context_menu/custom/invoice.png differ diff --git a/images/context_menu/custom/justice-scale.png b/images/context_menu/custom/justice-scale.png new file mode 100644 index 00000000..6f52915e Binary files /dev/null and b/images/context_menu/custom/justice-scale.png differ diff --git a/images/context_menu/custom/like.png b/images/context_menu/custom/like.png new file mode 100644 index 00000000..3625b936 Binary files /dev/null and b/images/context_menu/custom/like.png differ diff --git a/images/context_menu/custom/love-letter.png b/images/context_menu/custom/love-letter.png new file mode 100644 index 00000000..37529e46 Binary files /dev/null and b/images/context_menu/custom/love-letter.png differ diff --git a/images/context_menu/custom/policeman.png b/images/context_menu/custom/policeman.png new file mode 100644 index 00000000..847c2fc6 Binary files /dev/null and b/images/context_menu/custom/policeman.png differ diff --git a/images/context_menu/custom/printer.png b/images/context_menu/custom/printer.png new file mode 100644 index 00000000..42454c0c Binary files /dev/null and b/images/context_menu/custom/printer.png differ diff --git a/images/context_menu/custom/profit.png b/images/context_menu/custom/profit.png new file mode 100644 index 00000000..e82ad7ba Binary files /dev/null and b/images/context_menu/custom/profit.png differ diff --git a/images/context_menu/custom/puzzle-game.png b/images/context_menu/custom/puzzle-game.png new file mode 100644 index 00000000..a2cee86e Binary files /dev/null and b/images/context_menu/custom/puzzle-game.png differ diff --git a/images/context_menu/custom/radiation-hazard.png b/images/context_menu/custom/radiation-hazard.png new file mode 100644 index 00000000..1e151f1f Binary files /dev/null and b/images/context_menu/custom/radiation-hazard.png differ diff --git a/images/context_menu/custom/receipt.png b/images/context_menu/custom/receipt.png new file mode 100644 index 00000000..afecbb60 Binary files /dev/null and b/images/context_menu/custom/receipt.png differ diff --git a/images/context_menu/custom/recycle-sign.png b/images/context_menu/custom/recycle-sign.png new file mode 100644 index 00000000..d8094df3 Binary files /dev/null and b/images/context_menu/custom/recycle-sign.png differ diff --git a/images/context_menu/custom/science.png b/images/context_menu/custom/science.png new file mode 100644 index 00000000..0a4c4b76 Binary files /dev/null and b/images/context_menu/custom/science.png differ diff --git a/images/context_menu/custom/scissors.png b/images/context_menu/custom/scissors.png new file mode 100644 index 00000000..4bd963fb Binary files /dev/null and b/images/context_menu/custom/scissors.png differ diff --git a/images/context_menu/custom/star.png b/images/context_menu/custom/star.png new file mode 100644 index 00000000..beeeafd8 Binary files /dev/null and b/images/context_menu/custom/star.png differ diff --git a/images/context_menu/custom/start-up.png b/images/context_menu/custom/start-up.png new file mode 100644 index 00000000..4a86d4f4 Binary files /dev/null and b/images/context_menu/custom/start-up.png differ diff --git a/images/context_menu/custom/sticky-notes.png b/images/context_menu/custom/sticky-notes.png new file mode 100644 index 00000000..f04554fb Binary files /dev/null and b/images/context_menu/custom/sticky-notes.png differ diff --git a/images/context_menu/custom/target.png b/images/context_menu/custom/target.png new file mode 100644 index 00000000..94e4e8a7 Binary files /dev/null and b/images/context_menu/custom/target.png differ diff --git a/images/context_menu/custom/thermometer.png b/images/context_menu/custom/thermometer.png new file mode 100644 index 00000000..944bee94 Binary files /dev/null and b/images/context_menu/custom/thermometer.png differ diff --git a/images/context_menu/custom/trophy.png b/images/context_menu/custom/trophy.png new file mode 100644 index 00000000..937269a9 Binary files /dev/null and b/images/context_menu/custom/trophy.png differ diff --git a/images/context_menu/getcalendarevent.png b/images/context_menu/getcalendarevent.png new file mode 100644 index 00000000..e29dd993 Binary files /dev/null and b/images/context_menu/getcalendarevent.png differ diff --git a/images/context_menu/getcalendareventfromclipboard.png b/images/context_menu/getcalendareventfromclipboard.png new file mode 100644 index 00000000..f5f73283 Binary files /dev/null and b/images/context_menu/getcalendareventfromclipboard.png differ diff --git a/images/context_menu/gettask.png b/images/context_menu/gettask.png new file mode 100644 index 00000000..0905dd77 Binary files /dev/null and b/images/context_menu/gettask.png differ diff --git a/images/menu_spamfilter.png b/images/context_menu/spamfilter.png similarity index 100% rename from images/menu_spamfilter.png rename to images/context_menu/spamfilter.png diff --git a/images/menu_summarize.png b/images/context_menu/summarize.png similarity index 100% rename from images/menu_summarize.png rename to images/context_menu/summarize.png diff --git a/images/menu_translate.png b/images/context_menu/translate.png similarity index 100% rename from images/menu_translate.png rename to images/context_menu/translate.png diff --git a/js/mzta-menus.js b/js/mzta-menus.js index 32a8ef2d..07a779c1 100644 --- a/js/mzta-menus.js +++ b/js/mzta-menus.js @@ -36,6 +36,7 @@ import { cleanupNewlines, checkIfTagLabelExists, getConnectionType, + getContextMenuIcon, } from './mzta-utils.js' import { taPromptUtils } from './mzta-utils-prompt.js'; import { taLogger } from './mzta-logger.js'; @@ -75,8 +76,7 @@ export class mzta_Menus { this.rootMenu = []; this.shortcutMenu = []; this.menu_listeners = {}; - this.allPrompts = await getPrompts(true,also_special); - this.allPrompts.sort((a, b) => a.name.localeCompare(b.name)); + this.allPrompts = await getPrompts(true,also_special); this.allPrompts.forEach((prompt) => { this.addAction(prompt) }); @@ -219,7 +219,7 @@ export class mzta_Menus { // const tabs = await browser.tabs.query({ active: true, currentWindow: true }); // add custom text if needed - //browser.runtime.sendMessage({command: "chatgpt_open", prompt: fullPrompt, action: curr_prompt.action, tabId: tabs[0].id}); + // browser.runtime.sendMessage({command: "chatgpt_open", prompt: fullPrompt, action: curr_prompt.action, tabId: tabs[0].id}); if(curr_prompt.is_special == '1'){ // Special prompts switch(curr_prompt.id){ case 'prompt_add_tags': { // Add tags to the email @@ -495,15 +495,77 @@ export class mzta_Menus { } addShortcutMenu(prompt) { - let curr_menu_entry = {id: prompt.id, label: i18nConditionalGet(prompt.name), type: prompt.type}; + let curr_menu_entry = { + id: prompt.id, + label: i18nConditionalGet(prompt.name), + type: prompt.type, + show_in: prompt.show_in || "popup", + is_special: prompt.is_special, + position_display: prompt.position_display, + position_compose: prompt.position_compose, + position_context: prompt.position_context, + }; this.shortcutMenu.push(curr_menu_entry); } + async loadContextMenus() { + await browser.menus.removeAll(); + const contextPrompts = this.allPrompts.filter(p => { + const showIn = p.show_in || "popup"; + // Only show prompts that should appear in context menu and are for reading context (type 0 or 1) + return (showIn === "context" || showIn === "both") && (String(p.type) === "0" || String(p.type) === "1"); + }); + + if (contextPrompts.length === 0) { + this.logger.log("No prompts for context menu"); + return; + } + + // Create parent menu + await new Promise(resolve => + browser.menus.create({ + id: 'mzta-context-parent', + title: 'ThunderAI', + contexts: ["message_list"], + }, resolve) + ); + + // Sort by position_context if available, otherwise alphabetically + contextPrompts.sort((a, b) => { + const posA = a.position_context || 9999; + const posB = b.position_context || 9999; + if (posA !== posB) return posA - posB; + const nameA = i18nConditionalGet(a.name); + const nameB = i18nConditionalGet(b.name); + return nameA.localeCompare(nameB); + }); + + // Create child menu items + for (const prompt of contextPrompts) { + const title = i18nConditionalGet(prompt.name); + const iconPath = getContextMenuIcon(prompt); + const menuOpts = { + id: 'mzta-ctx-' + prompt.id, + title: title, + contexts: ["message_list"], + parentId: 'mzta-context-parent', + }; + if (iconPath) { + menuOpts.icons = iconPath; + } + await new Promise(resolve => + browser.menus.create(menuOpts, resolve) + ); + } + this.logger.log("Context menus loaded: " + contextPrompts.length + " items"); + } + async loadMenus(also_special = []) { await this.initialize(also_special); await this.addMenu(this.rootMenu); this.addClickListener(); this.loadShortcutMenu(); + await this.loadContextMenus(); this.logger.log("Menus loaded"); } diff --git a/js/mzta-prompts.js b/js/mzta-prompts.js index 57b14678..d081829b 100644 --- a/js/mzta-prompts.js +++ b/js/mzta-prompts.js @@ -62,6 +62,12 @@ summaryTabId (set by _openSummaryWebchat in mzta-background.js): The tab ID of the message display tab to update with the saved summary. + Show in menu (show_in attribute): + "popup": Show only in the popup menu + "context": Show only in the context menu + "both": Show in both popup and context menus + "none": Do not show in any menu + ================ USER PROPERTIES Enabled (enabled attribute): 0: Disabled @@ -108,6 +114,7 @@ const defaultPrompts = [ api_type: '', is_default: "1", is_special: "0", + show_in: "popup", }, { id: 'prompt_reply_advanced', @@ -126,6 +133,7 @@ const defaultPrompts = [ api_type: '', is_default: "1", is_special: "0", + show_in: "popup", }, { id: 'prompt_reply_custom_command', @@ -144,6 +152,7 @@ const defaultPrompts = [ api_type: '', is_default: "1", is_special: "0", + show_in: "popup", }, { id: 'prompt_rewrite_polite', @@ -162,6 +171,7 @@ const defaultPrompts = [ api_type: '', is_default: "1", is_special: "0", + show_in: "popup", }, { id: 'prompt_rewrite_formal', @@ -180,6 +190,7 @@ const defaultPrompts = [ api_type: '', is_default: "1", is_special: "0", + show_in: "popup", }, { id: 'prompt_classify', @@ -198,24 +209,7 @@ const defaultPrompts = [ api_type: '', is_default: "1", is_special: "0", - }, - { - id: 'prompt_summarize_this', - name: "__MSG_prompt_summarize_this__", - text: "prompt_summarize_this_full_text", - type: "0", - action: "0", - need_selected: "0", - need_signature: "0", - need_custom_text: "0", - define_response_lang: "1", - use_diff_viewer: "0", - chatgpt_web_model: '', - chatgpt_web_project: '', - chatgpt_web_custom_gpt: '', - api_type: '', - is_default: "1", - is_special: "0", + show_in: "popup", }, { id: 'prompt_proofread_this', @@ -234,6 +228,7 @@ const defaultPrompts = [ api_type: '', is_default: "1", is_special: "0", + show_in: "popup", }, { id: 'prompt_this', @@ -252,6 +247,7 @@ const defaultPrompts = [ api_type: '', is_default: "1", is_special: "0", + show_in: "popup", }, ]; @@ -270,6 +266,7 @@ const specialPrompts = [ api_type: '', is_default: "1", is_special: "1", + show_in: "both", }, { id: 'prompt_get_calendar_event', @@ -285,6 +282,7 @@ const specialPrompts = [ api_type: '', is_default: "1", is_special: "1", + show_in: "popup", }, { id: 'prompt_get_calendar_event_from_clipboard', @@ -300,6 +298,7 @@ const specialPrompts = [ api_type: '', is_default: "1", is_special: "1", + show_in: "popup", }, { id: 'prompt_get_task', @@ -315,6 +314,7 @@ const specialPrompts = [ api_type: '', is_default: "1", is_special: "1", + show_in: "popup", }, { id: 'prompt_spamfilter', @@ -330,6 +330,7 @@ const specialPrompts = [ api_type: '', is_default: "1", is_special: "1", + show_in: "context", }, { id: 'prompt_summarize', @@ -346,6 +347,7 @@ const specialPrompts = [ api_model: '', is_default: "1", is_special: "1", + show_in: "context", }, { id: 'prompt_summarize_email_template', @@ -362,6 +364,7 @@ const specialPrompts = [ api_model: '', is_default: "1", is_special: "1", + show_in: "none", }, { id: 'prompt_summarize_email_separator', @@ -378,6 +381,7 @@ const specialPrompts = [ api_model: '', is_default: "1", is_special: "1", + show_in: "none", }, { id: 'prompt_translate_this', @@ -394,6 +398,7 @@ const specialPrompts = [ api_model: '', is_default: "1", is_special: "1", + show_in: "context", } ]; @@ -464,7 +469,7 @@ export function preparePromptsForExport(prompts, include_api_settings = false){ } if(prompt.is_default == 1){ - let allowedKeys = ['id', 'enabled', 'position_compose', 'position_display', 'need_custom_text']; + let allowedKeys = ['id', 'enabled', 'position_compose', 'position_display', 'position_context', 'need_custom_text', 'show_in', 'custom_icon']; if(include_api_settings){ allowedKeys.push('api_type'); for (const [integration, options] of Object.entries(integration_options_config)) { @@ -517,6 +522,7 @@ async function getDefaultPrompts_withProps() { prompt.text = browser.i18n.getMessage(prompt.text); prompt.position_display = pos; prompt.position_compose = pos; + prompt.position_context = pos; prompt.enabled = 1; pos++; }) @@ -528,15 +534,19 @@ async function getDefaultPrompts_withProps() { if(prefs._default_prompts_properties?.[prompt.id]){ prompt.position_compose = prefs._default_prompts_properties[prompt.id].position_compose; prompt.position_display = prefs._default_prompts_properties[prompt.id].position_display; + prompt.position_context = prefs._default_prompts_properties[prompt.id]?.position_context || prompt.position_display; prompt.enabled = prefs._default_prompts_properties[prompt.id].enabled; prompt.need_custom_text = prefs._default_prompts_properties[prompt.id].need_custom_text; prompt.chatgpt_web_model = prefs._default_prompts_properties[prompt.id].chatgpt_web_model; prompt.chatgpt_web_project = prefs._default_prompts_properties[prompt.id].chatgpt_web_project; prompt.chatgpt_web_custom_gpt = (prefs._default_prompts_properties[prompt.id]?.chatgpt_web_custom_gpt || '').trim(); prompt.api_type = (prefs._default_prompts_properties[prompt.id]?.api_type || '').trim(); + prompt.show_in = prefs._default_prompts_properties[prompt.id]?.show_in || prompt.show_in; + prompt.custom_icon = prefs._default_prompts_properties[prompt.id]?.custom_icon || ""; }else{ prompt.position_display = pos; prompt.position_compose = pos; + prompt.position_context = pos; prompt.enabled = 1; pos++; } @@ -569,6 +579,12 @@ async function getCustomPrompts() { if(prompt.api_type === undefined){ prompt.api_type = ""; } + if(prompt.show_in === undefined){ + prompt.show_in = "popup"; + } + if(prompt.custom_icon === undefined){ + prompt.custom_icon = ""; + } }); return prefs._custom_prompt; } @@ -580,12 +596,15 @@ export async function setDefaultPromptsProperties(prompts) { default_prompts_properties[prompt.id] = { position_compose: (prompt.position_compose === undefined || prompt.position_compose === "undefined") ? "" : prompt.position_compose, position_display: (prompt.position_display === undefined || prompt.position_display === "undefined") ? "" : prompt.position_display, + position_context: (prompt.position_context === undefined || prompt.position_context === "undefined") ? "" : prompt.position_context, enabled: (prompt.enabled === undefined || prompt.enabled === "undefined") ? "" : prompt.enabled, need_custom_text: (prompt.need_custom_text === undefined || prompt.need_custom_text === "undefined") ? "" : prompt.need_custom_text, chatgpt_web_model: (prompt.chatgpt_web_model === undefined || prompt.chatgpt_web_model === "undefined") ? "" : prompt.chatgpt_web_model, chatgpt_web_project: (prompt.chatgpt_web_project === undefined || prompt.chatgpt_web_project === "undefined") ? "" : prompt.chatgpt_web_project, chatgpt_web_custom_gpt: (prompt.chatgpt_web_custom_gpt === undefined || prompt.chatgpt_web_custom_gpt === "undefined") ? "" : prompt.chatgpt_web_custom_gpt, api_type: (prompt.api_type === undefined || prompt.api_type === "undefined") ? "" : prompt.api_type, + show_in: (prompt.show_in === undefined || prompt.show_in === "undefined") ? "popup" : prompt.show_in, + custom_icon: (prompt.custom_icon === undefined || prompt.custom_icon === "undefined") ? "" : prompt.custom_icon, }; }); //console.log('>>>>>>>>>>>>>> default_prompts_properties: ' + JSON.stringify(default_prompts_properties)); @@ -616,6 +635,13 @@ export async function getSpecialPrompts(){ updatedPrompts.push(newPrompt); } }); + // Migrate: add show_in if missing from saved special prompts + updatedPrompts.forEach((prompt) => { + if (prompt.show_in === undefined) { + prompt.show_in = "both"; + } + }); + // console.log(">>>>>>>>>>>>> getSpecialPrompts updatedPrompts: " + JSON.stringify(updatedPrompts)); if (updatedPrompts.length !== prefs._special_prompts.length) { await browser.storage.local.set({ _special_prompts: updatedPrompts }); @@ -630,6 +656,56 @@ export async function setSpecialPrompts(prompts) { await browser.storage.local.set({_special_prompts: prompts}); } +export function getHiddenSpecialPromptIds() { + return specialPrompts.filter(p => p.show_in === "none").map(p => p.id); +} + +// Migration: if dynamic_menu_order_alphabet was true (or unset), assign initial positions +// so that prompts appear alphabetically with special prompts first, then disable the flag +// to switch to position-based ordering permanently. +export async function migrateMenuOrderAlphabetic() { + const prefs = await browser.storage.sync.get({ dynamic_menu_order_alphabet: true }); + if (!prefs.dynamic_menu_order_alphabet) { + return; + } + + const allPrompts = await getPrompts(false, [], true); + const hiddenSpecialIds = getHiddenSpecialPromptIds(); + const visiblePrompts = allPrompts.filter(p => !hiddenSpecialIds.includes(p.id)); + + const resolveName = (p) => { + const n = p.name || ''; + if (n.startsWith('__MSG_') && n.endsWith('__')) { + return browser.i18n.getMessage(n.substring(6, n.length - 2)); + } + return n; + }; + + const specials = visiblePrompts.filter(p => String(p.is_special) === '1') + .sort((a, b) => resolveName(a).localeCompare(resolveName(b))); + const others = visiblePrompts.filter(p => String(p.is_special) !== '1') + .sort((a, b) => resolveName(a).localeCompare(resolveName(b))); + const ordered = specials.concat(others); + + ordered.forEach((prompt, idx) => { + const pos = idx + 1; + prompt.position_display = pos; + prompt.position_compose = pos; + prompt.position_context = pos; + }); + + const defaultPromptsToSave = ordered.filter(p => String(p.is_default) === '1' && String(p.is_special) !== '1'); + const customPromptsToSave = ordered.filter(p => String(p.is_default) === '0' && String(p.is_special) !== '1'); + const visibleSpecialsToSave = ordered.filter(p => String(p.is_special) === '1'); + const hiddenSpecialsToPreserve = allPrompts.filter(p => hiddenSpecialIds.includes(p.id)); + + await setDefaultPromptsProperties(defaultPromptsToSave); + await setCustomPrompts(customPromptsToSave); + await setSpecialPrompts(visibleSpecialsToSave.concat(hiddenSpecialsToPreserve)); + + await browser.storage.sync.set({ dynamic_menu_order_alphabet: false }); +} + export async function getSpamFilterPrompt(){ return (await getSpecialPrompts()).find(prompt => prompt.id == 'prompt_spamfilter'); } diff --git a/js/mzta-utils.js b/js/mzta-utils.js index 9f49155f..6668cfde 100644 --- a/js/mzta-utils.js +++ b/js/mzta-utils.js @@ -16,24 +16,69 @@ * along with this program. If not, see . */ -import { prefs_default, getDynamicSettingValue } from '../options/mzta-options-default.js'; +import { + prefs_default, + getDynamicSettingValue +} from '../options/mzta-options-default.js'; + +import { customMenuIconsPath } from '../pages/menu_order/mzta-custom-menu-icons.js' + const sparks_min = '1.2.0'; // Minimum version of ThunderAI-Sparks required for the add-on to work const MICZ_IT_LOCALIZED_LANGS = ['es', 'de', 'fr', 'it']; export const getMenuContextCompose = () => 'compose_action_menu'; export const getMenuContextDisplay = () => 'message_display_action_menu'; +export const contextMenuID_GetCalendarEvent = 'mzta-get-calendar-event'; +export const contextMenuID_GetCalendarEventFromClipboard = 'mzta-get-calendar-event-from-clipboard'; +export const contextMenuID_GetTask = 'mzta-get-task'; export const contextMenuID_AddTags = 'mzta-add-tags'; export const contextMenuID_Spamfilter = 'mzta-spamfilter'; export const contextMenuID_Summarize = 'mzta-summarize'; export const contextMenuID_Translate = 'mzta-translate'; export const contextMenuIconsPath = { - [contextMenuID_AddTags]: 'moz-extension:images/menu_autotags.png', - [contextMenuID_Spamfilter]: 'moz-extension:images/menu_spamfilter.png', - [contextMenuID_Summarize]: 'moz-extension:images/menu_summarize.png', - [contextMenuID_Translate]: 'moz-extension:images/menu_translate.png', + [contextMenuID_GetCalendarEvent]: 'moz-extension:images/context_menu/getcalendarevent.png', + [contextMenuID_GetCalendarEventFromClipboard]: 'moz-extension:images/context_menu/getcalendareventfromclipboard.png', + [contextMenuID_GetTask]: 'moz-extension:images/context_menu/gettask.png', + [contextMenuID_AddTags]: 'moz-extension:images/context_menu/autotags.png', + [contextMenuID_Spamfilter]: 'moz-extension:images/context_menu/spamfilter.png', + [contextMenuID_Summarize]: 'moz-extension:images/context_menu/summarize.png', + [contextMenuID_Translate]: 'moz-extension:images/context_menu/translate.png', }; +// Map from special prompt IDs to context menu IDs +export const specialPromptToContextMenuID = { + 'prompt_get_calendar_event': contextMenuID_GetCalendarEvent, + 'prompt_get_calendar_event_from_clipboard': contextMenuID_GetCalendarEventFromClipboard, + 'prompt_get_task': contextMenuID_GetTask, + 'prompt_add_tags': contextMenuID_AddTags, + 'prompt_spamfilter': contextMenuID_Spamfilter, + 'prompt_summarize': contextMenuID_Summarize, + 'prompt_translate_this': contextMenuID_Translate, +}; + +// const defaultContextMenuIcon = 'moz-extension:images/icon-32px.png'; +const defaultContextMenuIcon = ''; + +export function getContextMenuIcon(prompt) { + // Back-compat: accept a plain id string too + const promptId = (typeof prompt === 'string') ? prompt : prompt?.id; + const isSpecial = (typeof prompt === 'object' && prompt !== null) ? String(prompt.is_special) === '1' : true; + + if (isSpecial) { + const contextMenuId = specialPromptToContextMenuID[promptId]; + if (contextMenuId && contextMenuIconsPath[contextMenuId]) { + return contextMenuIconsPath[contextMenuId]; + } + } + + if (typeof prompt === 'object' && prompt !== null && prompt.custom_icon) { + return 'moz-extension:' + customMenuIconsPath + prompt.custom_icon; + } + + return defaultContextMenuIcon; +} + export function getLanguageDisplayName(languageCode) { const languageDisplay = new Intl.DisplayNames([languageCode], {type: 'language'}); let lang_string = languageDisplay.of(languageCode); @@ -612,10 +657,11 @@ export function getActiveSpecialPromptsIDs(args = {}) { get_calendar_event = false, get_calendar_event_from_clipboard = false, get_task = false, + spamfilter = false, + summarize = false, + translate = false, is_chatgpt_web = false } = args; - // The Antispam filter is not here, because this method is used only - // to reload the ThunderAI button menu, not the context menu let output = []; // console.log(">>>>>>>>>> getActiveSpecialPromptsIDs args: " + JSON.stringify(args)); if (is_chatgpt_web) { @@ -629,13 +675,22 @@ export function getActiveSpecialPromptsIDs(args = {}) { } if (get_calendar_event) { output.push('prompt_get_calendar_event'); - } - if (get_calendar_event_from_clipboard) { - output.push('prompt_get_calendar_event_from_clipboard'); + if (get_calendar_event_from_clipboard) { + output.push('prompt_get_calendar_event_from_clipboard'); + } } if (get_task) { output.push('prompt_get_task'); } + if (spamfilter) { + output.push('prompt_spamfilter'); + } + if (summarize) { + output.push('prompt_summarize'); + } + if (translate) { + output.push('prompt_translate_this'); + } // console.log(">>>>>>>>>> getActiveSpecialPromptsIDs output: " + JSON.stringify(output)); return output; } diff --git a/mzta-background.js b/mzta-background.js index b1501aab..7759ff7c 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -42,24 +42,19 @@ import { getMessages, getMailBody, extractJsonObject, - contextMenuID_AddTags, - contextMenuID_Spamfilter, - contextMenuID_Summarize, - contextMenuID_Translate, - contextMenuIconsPath, sanitizeChatGPTModelData, sanitizeChatGPTWebCustomData, stripHtmlKeepLines, htmlBodyToPlainText, convertNewlinesToParagraphs, getConnectionType, - checkAPIIntegration, hasSpecificIntegration, } from './js/mzta-utils.js'; import { taPromptUtils } from './js/mzta-utils-prompt.js'; import { mzta_specialCommand } from './js/mzta-special-commands.js'; import { - getSpamFilterPrompt + getSpamFilterPrompt, + migrateMenuOrderAlphabetic } from './js/mzta-prompts.js'; import { taSpamReport } from './js/mzta-spamreport.js'; import { taSummaryStore } from './js/mzta-summarystore.js'; @@ -105,6 +100,9 @@ let special_prompts_ids = getActiveSpecialPromptsIDs({ get_calendar_event: doGetSparkFeature(prefs_init.get_calendar_event), get_calendar_event_from_clipboard: doGetSparkFeature(prefs_init.get_calendar_event_from_clipboard), get_task: doGetSparkFeature(prefs_init.get_task), + spamfilter: prefs_init.spamfilter, + summarize: prefs_init.summarize, + translate: prefs_init.translate, is_chatgpt_web: (prefs_init.connection_type === "chatgpt_web") }); @@ -169,7 +167,7 @@ function preparePopupMenu(tab) { } async function _reload_menus() { - let prefs_reload = await browser.storage.sync.get({add_tags: prefs_default.add_tags, get_calendar_event: prefs_default.get_calendar_event, get_calendar_event_from_clipboard: prefs_default.get_calendar_event_from_clipboard, get_task: prefs_default.get_task, connection_type: prefs_default.connection_type}); + let prefs_reload = await browser.storage.sync.get({add_tags: prefs_default.add_tags, get_calendar_event: prefs_default.get_calendar_event, get_calendar_event_from_clipboard: prefs_default.get_calendar_event_from_clipboard, get_task: prefs_default.get_task, connection_type: prefs_default.connection_type, spamfilter: prefs_default.spamfilter, summarize: prefs_default.summarize, translate: prefs_default.translate}); let getCalendarEvent = doGetSparkFeature(prefs_reload.get_calendar_event); let getCalendarEventFromClipboard = doGetSparkFeature(prefs_reload.get_calendar_event_from_clipboard); let getTask = doGetSparkFeature(prefs_reload.get_task); @@ -179,6 +177,9 @@ async function _reload_menus() { get_calendar_event: getCalendarEvent, get_calendar_event_from_clipboard: getCalendarEventFromClipboard, get_task: getTask, + spamfilter: prefs_reload.spamfilter, + summarize: prefs_reload.summarize, + translate: prefs_reload.translate, is_chatgpt_web: (prefs_reload.connection_type === "chatgpt_web") }); menus.reload(special_prompts_ids); @@ -186,6 +187,24 @@ async function _reload_menus() { return true; } +async function _getActiveSpecialIds() { + let prefs_reload = await browser.storage.sync.get({add_tags: prefs_default.add_tags, get_calendar_event: prefs_default.get_calendar_event, get_calendar_event_from_clipboard: prefs_default.get_calendar_event_from_clipboard, get_task: prefs_default.get_task, connection_type: prefs_default.connection_type, spamfilter: prefs_default.spamfilter, summarize: prefs_default.summarize, translate: prefs_default.translate}); + let getCalendarEvent = doGetSparkFeature(prefs_reload.get_calendar_event); + let getCalendarEventFromClipboard = doGetSparkFeature(prefs_reload.get_calendar_event_from_clipboard); + let getTask = doGetSparkFeature(prefs_reload.get_task); + return getActiveSpecialPromptsIDs({ + addtags: prefs_reload.add_tags, + addtags_api: hasSpecificIntegration(prefs_init.add_tags_use_specific_integration, prefs_init.add_tags_connection_type), + get_calendar_event: getCalendarEvent, + get_calendar_event_from_clipboard: getCalendarEventFromClipboard, + get_task: getTask, + spamfilter: prefs_reload.spamfilter, + summarize: prefs_reload.summarize, + translate: prefs_reload.translate, + is_chatgpt_web: (prefs_reload.connection_type === "chatgpt_web") + }); +} + async function _assign_tags(_data, create_new_tags = true, exclusions_exact_match = false) { let all_tags_list = await getTagsList(); all_tags_list = all_tags_list[1]; @@ -507,8 +526,26 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { case 'reload_menus': return _reload_menus(); break; + case 'get_active_special_ids': + return _getActiveSpecialIds(); + break; case 'shortcut_do_prompt': taLog.log("Executing shortcut, promptId: " + message.promptId); + if (message.promptId !== 'prompt_add_tags' && specialContextMenuActions[message.promptId]) { //TODO Add an option here if you want the user to decide to use the autotagging also in the popup menu + async function _shortcut_special() { + let tabId = message.tabId; + if (!tabId) { + let tabs = await browser.tabs.query({ active: true, currentWindow: true }); + if (tabs.length === 0) return false; + tabId = tabs[0].id; + } + let displayedMessage = await browser.messageDisplay.getDisplayedMessage(tabId); + if (!displayedMessage) return false; + taLog.log("Displayed message found."); + return specialContextMenuActions[message.promptId]([displayedMessage]); + } + return _shortcut_special(); + } return menus.executeMenuAction(message.promptId); break; case 'popup_menu_ready': @@ -1436,6 +1473,9 @@ function setupStorageChangeListener() { get_calendar_event: getCalendarEvent, get_calendar_event_from_clipboard: getCalendarEventFromClipboard, get_task: getTask, + spamfilter: prefs_init.spamfilter, + summarize: prefs_init.summarize, + translate: prefs_init.translate, is_chatgpt_web: (prefs_init.connection_type === "chatgpt_web") }); menus.reload(special_prompts_ids); @@ -1453,8 +1493,11 @@ function setupStorageChangeListener() { get_calendar_event: getCalendarEvent, get_calendar_event_from_clipboard: getCalendarEventFromClipboard, get_task: getTask, + spamfilter: prefs_init.spamfilter, + summarize: prefs_init.summarize, + translate: prefs_init.translate, is_chatgpt_web: (prefs_init.connection_type === "chatgpt_web") - }); + }); menus.reload(special_prompts_ids); } @@ -1470,8 +1513,11 @@ function setupStorageChangeListener() { get_calendar_event: getCalendarEvent, get_calendar_event_from_clipboard: getCalendarEventFromClipboard, get_task: getTask, + spamfilter: prefs_init.spamfilter, + summarize: prefs_init.summarize, + translate: prefs_init.translate, is_chatgpt_web: (prefs_init.connection_type === "chatgpt_web") - }); + }); menus.reload(special_prompts_ids); } @@ -1487,8 +1533,71 @@ function setupStorageChangeListener() { get_calendar_event: getCalendarEvent, get_calendar_event_from_clipboard: getCalendarEventFromClipboard, get_task: getTask, + spamfilter: prefs_init.spamfilter, + summarize: prefs_init.summarize, + translate: prefs_init.translate, is_chatgpt_web: (prefs_init.connection_type === "chatgpt_web") - }); + }); + menus.reload(special_prompts_ids); + } + + // Process 'spamfilter' changes + if (changes.spamfilter) { + const newSpamfilter = changes.spamfilter.newValue; + let getCalendarEvent = doGetSparkFeature(prefs_init.get_calendar_event); + let getCalendarEventFromClipboard = doGetSparkFeature(prefs_init.get_calendar_event_from_clipboard); + let getTask = doGetSparkFeature(prefs_init.get_task); + const special_prompts_ids = getActiveSpecialPromptsIDs({ + addtags: prefs_init.add_tags, + addtags_api: hasSpecificIntegration(prefs_init.add_tags_use_specific_integration, prefs_init.add_tags_connection_type), + get_calendar_event: getCalendarEvent, + get_calendar_event_from_clipboard: getCalendarEventFromClipboard, + get_task: getTask, + spamfilter: newSpamfilter, + summarize: prefs_init.summarize, + translate: prefs_init.translate, + is_chatgpt_web: (prefs_init.connection_type === "chatgpt_web") + }); + menus.reload(special_prompts_ids); + } + + // Process 'summarize' changes + if (changes.summarize) { + const newSummarize = changes.summarize.newValue; + let getCalendarEvent = doGetSparkFeature(prefs_init.get_calendar_event); + let getCalendarEventFromClipboard = doGetSparkFeature(prefs_init.get_calendar_event_from_clipboard); + let getTask = doGetSparkFeature(prefs_init.get_task); + const special_prompts_ids = getActiveSpecialPromptsIDs({ + addtags: prefs_init.add_tags, + addtags_api: hasSpecificIntegration(prefs_init.add_tags_use_specific_integration, prefs_init.add_tags_connection_type), + get_calendar_event: getCalendarEvent, + get_calendar_event_from_clipboard: getCalendarEventFromClipboard, + get_task: getTask, + spamfilter: prefs_init.spamfilter, + summarize: newSummarize, + translate: prefs_init.translate, + is_chatgpt_web: (prefs_init.connection_type === "chatgpt_web") + }); + menus.reload(special_prompts_ids); + } + + // Process 'translate' changes + if (changes.translate) { + const newTranslate = changes.translate.newValue; + let getCalendarEvent = doGetSparkFeature(prefs_init.get_calendar_event); + let getCalendarEventFromClipboard = doGetSparkFeature(prefs_init.get_calendar_event_from_clipboard); + let getTask = doGetSparkFeature(prefs_init.get_task); + const special_prompts_ids = getActiveSpecialPromptsIDs({ + addtags: prefs_init.add_tags, + addtags_api: hasSpecificIntegration(prefs_init.add_tags_use_specific_integration, prefs_init.add_tags_connection_type), + get_calendar_event: getCalendarEvent, + get_calendar_event_from_clipboard: getCalendarEventFromClipboard, + get_task: getTask, + spamfilter: prefs_init.spamfilter, + summarize: prefs_init.summarize, + translate: newTranslate, + is_chatgpt_web: (prefs_init.connection_type === "chatgpt_web") + }); menus.reload(special_prompts_ids); } @@ -1504,14 +1613,15 @@ function setupStorageChangeListener() { get_calendar_event: getCalendarEvent, get_calendar_event_from_clipboard: getCalendarEventFromClipboard, get_task: getTask, + spamfilter: prefs_init.spamfilter, + summarize: prefs_init.summarize, + translate: prefs_init.translate, is_chatgpt_web: (newConnectionType === "chatgpt_web") - }); + }); menus.reload(special_prompts_ids); } - reload_pref_init().then(() => { - addContextMenuItems(); - }); + reload_pref_init(); } }); } @@ -1540,97 +1650,34 @@ function setupPermissionsRemovedListener() { setupPermissionsRemovedListener(); // Menus handling +await migrateMenuOrderAlphabetic(); const menus = new mzta_Menus(openChatGPT, prefs_init.do_debug); menus.loadMenus(special_prompts_ids); -// Context Menus -function addContextMenu(menu_id) { - browser.menus.remove(menu_id); - browser.menus.create({ - id: menu_id, - title: browser.i18n.getMessage("context_menu_" + menu_id), - contexts: ["message_list"], - icons: contextMenuIconsPath[menu_id], - }); - taLog.log("Context menu added: " + menu_id); - // console.log(">>>>>>> contextMenuIconsPath[menu_id]: " + contextMenuIconsPath[menu_id]); -} +// Context menu click handling +// Context menus are now created dynamically by mzta_Menus.loadContextMenus() +// based on each prompt's show_in property. The menu item IDs use the format 'mzta-ctx-'. +// Special prompts (add_tags, spamfilter, summarize, translate) are routed to processEmails() +// for batch processing. Regular prompts are executed via menus.executeMenuAction(). -function removeContextMenu(menu_id) { - browser.menus.remove(menu_id); - taLog.log("Context menu removed: " + menu_id); -} +const specialContextMenuActions = { + 'prompt_add_tags': (messages) => processEmails({ messages, addTagsAuto: true }), + 'prompt_spamfilter': (messages) => processEmails({ messages, spamFilter: true }), + 'prompt_summarize': (messages) => processEmails({ messages, summarize: true }), + 'prompt_translate_this': (messages) => processEmails({ messages, translate: true }), +}; -function addContextMenuItems() { - let itemsToAdd = []; +browser.menus.onClicked.addListener((info, tab) => { + const menuItemId = info.menuItemId; + if (typeof menuItemId !== 'string' || !menuItemId.startsWith('mzta-ctx-')) { + return; + } + const promptId = menuItemId.replace('mzta-ctx-', ''); - // Add Context menu: Add tags - if(prefs_init.add_tags && checkAPIIntegration(prefs_init.connection_type, prefs_init.add_tags_use_specific_integration,prefs_init.add_tags_connection_type)){ - itemsToAdd.push(contextMenuID_AddTags); + if (specialContextMenuActions[promptId]) { + specialContextMenuActions[promptId](getMessages(info.selectedMessages)); } else { - removeContextMenu(contextMenuID_AddTags); - } - - // Add Context menu: Spamfilter - if(prefs_init.spamfilter && checkAPIIntegration(prefs_init.connection_type, prefs_init.spamfilter_use_specific_integration,prefs_init.spamfilter_connection_type)){ - itemsToAdd.push(contextMenuID_Spamfilter); - } else { - removeContextMenu(contextMenuID_Spamfilter); - } - - // Add Context menu: Summarize - if(prefs_init.summarize && checkAPIIntegration(prefs_init.connection_type, prefs_init.summarize_use_specific_integration, prefs_init.summarize_connection_type)) { - itemsToAdd.push(contextMenuID_Summarize); - } else { - removeContextMenu(contextMenuID_Summarize); - } - - // Add Context menu: Translate - if(prefs_init.translate && checkAPIIntegration(prefs_init.connection_type, prefs_init.translate_use_specific_integration, prefs_init.translate_connection_type)){ - itemsToAdd.push(contextMenuID_Translate); - } else { - removeContextMenu(contextMenuID_Translate); - } - - itemsToAdd.sort((a, b) => { - let titleA = browser.i18n.getMessage("context_menu_" + a); - let titleB = browser.i18n.getMessage("context_menu_" + b); - return titleA.localeCompare(titleB); - }); - - itemsToAdd.forEach(menu_id => { - addContextMenu(menu_id); - }); -} - -addContextMenuItems(); - -// Listen for context menu item clicks -browser.menus.onClicked.addListener( (info, tab) => { - let _add_tags = false - let _spamfilter = false - let _summarize = false; - let _translate = false; - if(info.menuItemId === contextMenuID_AddTags){ - _add_tags = true; - } - if(info.menuItemId === contextMenuID_Spamfilter){ - _spamfilter = true; - } - if(info.menuItemId === contextMenuID_Summarize) { - _summarize = true; - } - if(info.menuItemId === contextMenuID_Translate) { - _translate = true; - } - if(_add_tags || _spamfilter || _summarize || _translate){ - processEmails({ - messages: getMessages(info.selectedMessages), - addTagsAuto: _add_tags, - spamFilter: _spamfilter, - summarize: _summarize, - translate: _translate - }); + menus.executeMenuAction(promptId); } }); diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index 72ed1b1c..cbc7eb0f 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -114,7 +114,6 @@ export const prefs_default = { chatgpt_web_custom_gpt: '', chatgpt_web_load_wait_time: 1000, dynamic_menu_force_enter: false, - dynamic_menu_order_alphabet: true, placeholders_use_default_value: false, max_prompt_length: 30000, // max string length for prompt add_tags: false, diff --git a/options/mzta-options.html b/options/mzta-options.html index 3513bb32..c31ae21b 100644 --- a/options/mzta-options.html +++ b/options/mzta-options.html @@ -117,12 +117,11 @@ diff --git a/options/mzta-options.js b/options/mzta-options.js index c54620ea..c8a4c2b3 100644 --- a/options/mzta-options.js +++ b/options/mzta-options.js @@ -410,6 +410,10 @@ document.addEventListener('DOMContentLoaded', async () => { openTab('/pages/customdataplaceholders/mzta-custom-dataplaceholders.html'); }); + document.getElementById('btnMenuOrder').addEventListener('click', () => { + openTab('/pages/menu_order/mzta-menu-order.html'); + }); + document.getElementById('btnManageTagsInfo').addEventListener('click', () => { openTab('/pages/addtags/mzta-add-tags.html'); }); diff --git a/options/mzta-release-notes.html b/options/mzta-release-notes.html index 6b890353..808eb6b1 100644 --- a/options/mzta-release-notes.html +++ b/options/mzta-release-notes.html @@ -22,6 +22,8 @@
  • Added the {%mail_full_headers%} placeholder to retrieve all the email headers at once [#713].
  • [All APIs] In the API webchat the status messages have different colors [#3].
  • Account exclusion lists for add tags and antispam are enforced only for automatic analysis of incoming emails and not for the context menu action that is always executed [#749].
  • +
  • [All APIs] Inline auto translation for emails added [#247].
  • +
  • Custom menus configuration added. Now it's possibile to define which prompts show in the ThunderAI menu, which ones in the context menu and in which order [#49, #184, #680].
  • ...
  • Version 4.0.3 - 20/03/2026

    diff --git a/pages/customprompts/mzta-custom-prompts.html b/pages/customprompts/mzta-custom-prompts.html index 52943f9d..3f9eefd2 100644 --- a/pages/customprompts/mzta-custom-prompts.html +++ b/pages/customprompts/mzta-custom-prompts.html @@ -51,6 +51,14 @@

    + +
    + +


    @@ -1095,6 +1116,16 @@ function loadPromptsList(values){ ` + `

    + __MSG_show_in__: +
    + ` + show_in_output + ` + ` + + ` +

    __MSG_customPrompts_form_label_Action__:
    ` + action_output + `