summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/unifiedtoolbar/modules
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/mail/components/unifiedtoolbar/modules/ButtonStyle.mjs23
-rw-r--r--comm/mail/components/unifiedtoolbar/modules/CustomizableItems.sys.mjs134
-rw-r--r--comm/mail/components/unifiedtoolbar/modules/CustomizableItemsDetails.mjs445
-rw-r--r--comm/mail/components/unifiedtoolbar/modules/CustomizationState.mjs55
-rw-r--r--comm/mail/components/unifiedtoolbar/modules/ToolbarMigration.sys.mjs419
5 files changed, 1076 insertions, 0 deletions
diff --git a/comm/mail/components/unifiedtoolbar/modules/ButtonStyle.mjs b/comm/mail/components/unifiedtoolbar/modules/ButtonStyle.mjs
new file mode 100644
index 0000000000..c65f3ed16a
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/modules/ButtonStyle.mjs
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Array of button styles with the class name at the index of the corresponding
+ * button style pref integer value.
+ *
+ * @type {Array<string>}
+ */
+export const BUTTON_STYLE_MAP = [
+ "icons-beside-text",
+ "icons-above-text",
+ "icons-only",
+ "text-only",
+];
+
+/**
+ * Name of preference that stores the button style as an integer.
+ *
+ * @type {string}
+ */
+export const BUTTON_STYLE_PREF = "toolbar.unifiedtoolbar.buttonstyle";
diff --git a/comm/mail/components/unifiedtoolbar/modules/CustomizableItems.sys.mjs b/comm/mail/components/unifiedtoolbar/modules/CustomizableItems.sys.mjs
new file mode 100644
index 0000000000..eb9ccee46f
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/modules/CustomizableItems.sys.mjs
@@ -0,0 +1,134 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import CUSTOMIZABLE_ITEMS from "resource:///modules/CustomizableItemsDetails.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+});
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "glodaEnabled",
+ "mailnews.database.global.indexer.enabled",
+ true,
+ () => Services.obs.notifyObservers(null, "unified-toolbar-state-change")
+);
+
+const DEFAULT_ITEMS = ["spacer", "search-bar", "spacer"];
+const DEFAULT_ITEMS_WITHOUT_SEARCH = ["spacer"];
+
+/**
+ * @type {{id: string, spaces: string[], installDate: Date}[]}
+ */
+const EXTENSIONS = [];
+
+export const EXTENSION_PREFIX = "ext-";
+
+/**
+ * Add an extension button that is available in the given spaces. Defaults to
+ * making the button only available in the mail space. To provide it in all
+ * spaces, pass an empty array for the spaces.
+ *
+ * @param {string} id - Extension ID to add the button for.
+ * @param {string[]} [spaces=["mail"]] - Array of spaces the button can be used
+ * in.
+ */
+export async function registerExtension(id, spaces = ["mail"]) {
+ if (EXTENSIONS.some(extension => extension.id === id)) {
+ return;
+ }
+ const addon = await lazy.AddonManager.getAddonByID(id);
+ EXTENSIONS.push({
+ id,
+ spaces,
+ installDate: addon?.installDate ?? new Date(),
+ });
+ EXTENSIONS.sort(
+ (extA, extB) => extA.installDate.valueOf() - extB.installDate.valueOf()
+ );
+}
+
+/**
+ * Remove the extension from the palette of available items.
+ *
+ * @param {string} id - Extension ID to remove.
+ */
+export function unregisterExtension(id) {
+ const index = EXTENSIONS.findIndex(extension => extension.id === id);
+ EXTENSIONS.splice(index, 1);
+}
+
+/**
+ * Get the IDs for the extension buttons available in a given space.
+ *
+ * @param {string} [space] - Space name, "default" or falsy value to specify the
+ * space the extension items should be returned for. For default, extensions
+ * explicitly available in the default space are returned. With a falsy value,
+ * extensions available in all spaces are returned.
+ * @param {boolean} [includeSpaceAgnostic=false] - When set, extensions that are
+ * available for all spaces and the provided space are returned. Only has an
+ * effect if space is not falsy.
+ * @returns {string[]} Array of item IDs for extensions in the given space.
+ */
+function getExtensionsForSpace(space, includeSpaceAgnostic = false) {
+ return EXTENSIONS.filter(
+ extension =>
+ (space && extension.spaces?.includes(space)) ||
+ ((!space || includeSpaceAgnostic) && !extension.spaces?.length)
+ ).map(extension => `${EXTENSION_PREFIX}${extension.id}`);
+}
+
+/**
+ * Get the items available for the unified toolbar in a given space.
+ *
+ * @param {string} [space] - ID of the space to get the available exclusive
+ * items of. When omitted only items allowed in all spaces are returned.
+ * @param {boolean} [includeSpaceAgnostic=false] - When set, extensions that are
+ * available for all spaces and the provided space are returned. Only has an
+ * effect if space is not falsy.
+ * @returns {string[]} Array of item IDs available in the space.
+ */
+export function getAvailableItemIdsForSpace(
+ space,
+ includeSpaceAgnostic = false
+) {
+ return CUSTOMIZABLE_ITEMS.filter(
+ item =>
+ ((space && item.spaces?.includes(space)) ||
+ ((!space || includeSpaceAgnostic) &&
+ (!item.spaces || item.spaces.length === 0))) &&
+ (item.id !== "search-bar" || lazy.glodaEnabled)
+ )
+ .map(item => item.id)
+ .concat(getExtensionsForSpace(space, includeSpaceAgnostic));
+}
+
+/**
+ * Retrieve the set of items that are in the default configuration of the
+ * toolbar for a given space.
+ *
+ * @param {string} space - ID of the space to get the default items for.
+ * "default" is passed to indicate a default state without any active space.
+ * @returns {string[]} Array of item IDs to show by default in the space.
+ */
+export function getDefaultItemIdsForSpace(space) {
+ return (
+ lazy.glodaEnabled ? DEFAULT_ITEMS : DEFAULT_ITEMS_WITHOUT_SEARCH
+ ).concat(getExtensionsForSpace(space, true));
+}
+
+/**
+ * Set of item IDs that can occur more than once in the targets of a space.
+ *
+ * @type {Set<string>}
+ */
+export const MULTIPLE_ALLOWED_ITEM_IDS = new Set(
+ CUSTOMIZABLE_ITEMS.filter(item => item.allowMultiple).map(item => item.id)
+);
+
+export const SKIP_FOCUS_ITEM_IDS = new Set(
+ CUSTOMIZABLE_ITEMS.filter(item => item.skipFocus).map(item => item.id)
+);
diff --git a/comm/mail/components/unifiedtoolbar/modules/CustomizableItemsDetails.mjs b/comm/mail/components/unifiedtoolbar/modules/CustomizableItemsDetails.mjs
new file mode 100644
index 0000000000..1e3900d6c3
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/modules/CustomizableItemsDetails.mjs
@@ -0,0 +1,445 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* This has the following companion definition files:
+ * - unifiedToolbarCustomizableItems.css for the preview icons based on the id.
+ * - unifiedToolbarItems.ftl for the labels associated with the labelId.
+ * - unifiedToolbarCustomizableItems.inc.xhtml for the templates referenced with
+ * templateId.
+ * - unifiedToolbarShared.css contains styles for the template contents shared
+ * between the customization preview and the actual toolbar.
+ * - unifiedtoolbar/content/items contains all item specific custom elements.
+ */
+
+/**
+ * @typedef {object} CustomizableItemDetails
+ * @property {string} id - The ID of the item. Will be set as a class on the
+ * outer wrapper. May not contain commas.
+ * @property {string} labelId - Fluent ID for the label shown while in the
+ * palette.
+ * @property {boolean} [allowMultiple] - If this item can be added more than
+ * once to a space.
+ * @property {string[]} [spaces] - If empty or omitted, item is allowed in all
+ * spaces.
+ * @property {string} [templateId] - ID of template defining the "live" markup.
+ * @property {string[]} [requiredModules] - List of modules that must be loaded
+ * for the template of this item.
+ * @property {boolean} [hasContextMenu] - Indicates that this item has its own
+ * context menu, and the global unified toolbar one shouldn't be shown.
+ * @property {boolean} [skipFocus] - If this item should be skipped in keyboard
+ * focus navigation.
+ */
+
+/**
+ * @type {CustomizableItemDetails[]}
+ */
+export default [
+ // Universal items (all spaces)
+ {
+ id: "spacer",
+ labelId: "spacer",
+ allowMultiple: true,
+ skipFocus: true,
+ },
+ {
+ // This item gets filtered out when gloda is disabled.
+ id: "search-bar",
+ labelId: "search-bar",
+ templateId: "searchBarItemTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/global-search-bar.mjs",
+ ],
+ hasContextMenu: true,
+ skipFocus: true,
+ },
+ {
+ id: "write-message",
+ labelId: "toolbar-write-message",
+ templateId: "writeMessageTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/unified-toolbar-button.mjs",
+ ],
+ },
+ {
+ id: "get-messages",
+ labelId: "toolbar-get-messages",
+ templateId: "getMessagesTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/unified-toolbar-button.mjs",
+ ],
+ },
+ {
+ id: "address-book",
+ labelId: "toolbar-address-book",
+ templateId: "addressBookTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/space-button.mjs",
+ ],
+ },
+ {
+ id: "chat",
+ labelId: "toolbar-chat",
+ templateId: "chatTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/space-button.mjs",
+ ],
+ },
+ {
+ id: "add-ons-and-themes",
+ labelId: "toolbar-add-ons-and-themes",
+ templateId: "addOnsAndThemesTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/addons-button.mjs",
+ ],
+ },
+ {
+ id: "calendar",
+ labelId: "toolbar-calendar",
+ templateId: "calendarTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/space-button.mjs",
+ ],
+ },
+ {
+ id: "tasks",
+ labelId: "toolbar-tasks",
+ templateId: "tasksTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/space-button.mjs",
+ ],
+ },
+ {
+ id: "mail",
+ labelId: "toolbar-mail",
+ templateId: "mailTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/space-button.mjs",
+ ],
+ },
+ {
+ id: "new-event",
+ labelId: "toolbar-new-event",
+ templateId: "newEventTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/unified-toolbar-button.mjs",
+ ],
+ },
+ {
+ id: "new-task",
+ labelId: "toolbar-new-task",
+ templateId: "newTaskTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/unified-toolbar-button.mjs",
+ ],
+ },
+ // Mail space
+ {
+ id: "move-to",
+ labelId: "toolbar-move-to",
+ spaces: ["mail"],
+ templateId: "moveToTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-tab-button.mjs",
+ ],
+ },
+ {
+ id: "reply",
+ labelId: "toolbar-reply",
+ spaces: ["mail"],
+ templateId: "replyTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-tab-button.mjs",
+ ],
+ },
+ {
+ id: "reply-all",
+ labelId: "toolbar-reply-all",
+ spaces: ["mail"],
+ templateId: "replyAllTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-tab-button.mjs",
+ ],
+ },
+ {
+ id: "reply-to-list",
+ labelId: "toolbar-reply-to-list",
+ spaces: ["mail"],
+ templateId: "replyToListTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/reply-list-button.mjs",
+ ],
+ },
+ {
+ id: "redirect",
+ labelId: "toolbar-redirect",
+ spaces: ["mail"],
+ templateId: "redirectTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-tab-button.mjs",
+ ],
+ },
+ {
+ id: "archive",
+ labelId: "toolbar-archive",
+ spaces: ["mail"],
+ templateId: "archiveTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-tab-button.mjs",
+ ],
+ },
+ {
+ id: "conversation",
+ labelId: "toolbar-conversation",
+ spaces: ["mail"],
+ templateId: "conversationTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-tab-button.mjs",
+ ],
+ },
+ {
+ id: "previous-unread",
+ labelId: "toolbar-previous-unread",
+ spaces: ["mail"],
+ templateId: "previousUnreadTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-tab-button.mjs",
+ ],
+ },
+ {
+ id: "previous",
+ labelId: "toolbar-previous",
+ spaces: ["mail"],
+ templateId: "previousTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-tab-button.mjs",
+ ],
+ },
+ {
+ id: "next-unread",
+ labelId: "toolbar-next-unread",
+ spaces: ["mail"],
+ templateId: "nextUnreadTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-tab-button.mjs",
+ ],
+ },
+ {
+ id: "next",
+ labelId: "toolbar-next",
+ spaces: ["mail"],
+ templateId: "nextTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-tab-button.mjs",
+ ],
+ },
+ {
+ id: "junk",
+ labelId: "toolbar-junk",
+ spaces: ["mail"],
+ templateId: "junkTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-tab-button.mjs",
+ ],
+ },
+ {
+ id: "delete",
+ labelId: "toolbar-delete",
+ spaces: ["mail"],
+ templateId: "deleteTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/delete-button.mjs",
+ ],
+ },
+ {
+ id: "compact",
+ labelId: "toolbar-compact",
+ spaces: ["mail"],
+ templateId: "compactTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/compact-folder-button.mjs",
+ ],
+ },
+ {
+ id: "add-as-event",
+ labelId: "toolbar-add-as-event",
+ spaces: ["mail"],
+ templateId: "addAsEventTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/add-to-calendar-button.mjs",
+ ],
+ },
+ {
+ id: "add-as-task",
+ labelId: "toolbar-add-as-task",
+ spaces: ["mail"],
+ templateId: "addAsTaskTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/add-to-calendar-button.mjs",
+ ],
+ },
+ {
+ id: "folder-location",
+ labelId: "toolbar-folder-location",
+ spaces: ["mail"],
+ templateId: "folderLocationTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/folder-location-button.mjs",
+ ],
+ },
+ {
+ id: "tag-message",
+ labelId: "toolbar-tag-message",
+ spaces: ["mail"],
+ templateId: "tagMessageTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-tab-button.mjs",
+ ],
+ },
+ {
+ id: "forward-inline",
+ labelId: "toolbar-forward-inline",
+ spaces: ["mail"],
+ templateId: "forwardInlineTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-tab-button.mjs",
+ ],
+ },
+ {
+ id: "forward-attachment",
+ labelId: "toolbar-forward-attachment",
+ spaces: ["mail"],
+ templateId: "forwardAttachmentTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-tab-button.mjs",
+ ],
+ },
+ {
+ id: "mark-as",
+ labelId: "toolbar-mark-as",
+ spaces: ["mail"],
+ templateId: "markAsTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-tab-button.mjs",
+ ],
+ },
+ {
+ id: "view-picker",
+ labelId: "toolbar-view-picker",
+ spaces: ["mail"],
+ templateId: "viewPickerTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/view-picker-button.mjs",
+ ],
+ },
+ {
+ id: "print",
+ labelId: "toolbar-print",
+ spaces: ["mail"],
+ templateId: "printTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-tab-button.mjs",
+ ],
+ },
+ {
+ id: "quick-filter-bar",
+ labelId: "toolbar-quick-filter-bar",
+ spaces: ["mail"],
+ templateId: "quickFilterBarTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/quick-filter-bar-toggle.mjs",
+ ],
+ },
+ {
+ id: "go-back",
+ labelId: "toolbar-go-back",
+ spaces: ["mail"],
+ templateId: "goBackTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-go-button.mjs",
+ ],
+ hasContextMenu: true,
+ },
+ {
+ id: "go-forward",
+ labelId: "toolbar-go-forward",
+ spaces: ["mail"],
+ templateId: "goForwardTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-go-button.mjs",
+ ],
+ hasContextMenu: true,
+ },
+ {
+ id: "stop",
+ labelId: "toolbar-stop",
+ spaces: ["mail"],
+ templateId: "stopTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/mail-tab-button.mjs",
+ ],
+ },
+ {
+ id: "throbber",
+ labelId: "toolbar-throbber",
+ spaces: ["mail"],
+ templateId: "throbberTemplate",
+ skipFocus: true,
+ },
+ // Calendar & Tasks space
+ {
+ id: "edit-event",
+ labelId: "toolbar-edit-event",
+ spaces: ["calendar", "tasks"],
+ templateId: "editEventTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/unified-toolbar-button.mjs",
+ ],
+ },
+ {
+ id: "synchronize",
+ labelId: "toolbar-synchronize",
+ spaces: ["calendar", "tasks"],
+ templateId: "synchronizeTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/unified-toolbar-button.mjs",
+ ],
+ },
+ {
+ id: "delete-event",
+ labelId: "toolbar-delete-event",
+ spaces: ["calendar", "tasks"],
+ templateId: "deleteEventTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/unified-toolbar-button.mjs",
+ ],
+ },
+ {
+ id: "print-event",
+ labelId: "toolbar-print-event",
+ spaces: ["calendar", "tasks"],
+ templateId: "printEventTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/unified-toolbar-button.mjs",
+ ],
+ },
+ // Calendar space
+ {
+ id: "go-to-today",
+ labelId: "toolbar-go-to-today",
+ spaces: ["calendar"],
+ templateId: "goToTodayTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/unified-toolbar-button.mjs",
+ ],
+ },
+ {
+ id: "unifinder",
+ labelId: "toolbar-unifinder",
+ spaces: ["calendar"],
+ templateId: "calendarUnifinderTemplate",
+ requiredModules: [
+ "chrome://messenger/content/unifiedtoolbar/unified-toolbar-button.mjs",
+ ],
+ },
+];
diff --git a/comm/mail/components/unifiedtoolbar/modules/CustomizationState.mjs b/comm/mail/components/unifiedtoolbar/modules/CustomizationState.mjs
new file mode 100644
index 0000000000..75c0b390be
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/modules/CustomizationState.mjs
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const MAIN_WINDOW_DOCUMENT = "chrome://messenger/content/messenger.xhtml";
+const UNIFIED_TOOLBAR_ID = "unifiedToolbar";
+const CUSTOMIZATION_ATTRIBUTE_NAME = "state";
+
+/**
+ * @typedef {object} UnifiedToolbarCustomizationState
+ * @property {string[]} (spaceName) - Each space has a key on the object,
+ * containing an ordered array of item IDs.
+ */
+
+/**
+ * Store the customization state for the unified toolbar. Sends a global
+ * observer notification.
+ *
+ * @param {UnifiedToolbarCustomizationState} state
+ */
+export function storeState(state) {
+ Services.xulStore.setValue(
+ MAIN_WINDOW_DOCUMENT,
+ UNIFIED_TOOLBAR_ID,
+ CUSTOMIZATION_ATTRIBUTE_NAME,
+ JSON.stringify(state)
+ );
+ Services.obs.notifyObservers(null, "unified-toolbar-state-change");
+}
+
+/**
+ * Retrieve the customization state of the unified toolbar.
+ *
+ * @returns {UnifiedToolbarCustomizationState} A partial representation of the
+ * customization state of the unified toolbar. Missing spaces are in their
+ * default states.
+ */
+export function getState() {
+ let state = {};
+ if (
+ Services.xulStore.hasValue(
+ MAIN_WINDOW_DOCUMENT,
+ UNIFIED_TOOLBAR_ID,
+ CUSTOMIZATION_ATTRIBUTE_NAME
+ )
+ ) {
+ const rawState = Services.xulStore.getValue(
+ MAIN_WINDOW_DOCUMENT,
+ UNIFIED_TOOLBAR_ID,
+ CUSTOMIZATION_ATTRIBUTE_NAME
+ );
+ state = JSON.parse(rawState);
+ }
+ return state;
+}
diff --git a/comm/mail/components/unifiedtoolbar/modules/ToolbarMigration.sys.mjs b/comm/mail/components/unifiedtoolbar/modules/ToolbarMigration.sys.mjs
new file mode 100644
index 0000000000..117fb774ba
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/modules/ToolbarMigration.sys.mjs
@@ -0,0 +1,419 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import {
+ getState,
+ storeState,
+} from "resource:///modules/CustomizationState.mjs";
+import {
+ MULTIPLE_ALLOWED_ITEM_IDS,
+ EXTENSION_PREFIX,
+ getAvailableItemIdsForSpace,
+ getDefaultItemIdsForSpace,
+} from "resource:///modules/CustomizableItems.sys.mjs";
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ExtensionCommon: "resource://gre/modules/ExtensionCommon.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ getCachedAllowedSpaces: "resource:///modules/ExtensionToolbarButtons.jsm",
+ setCachedAllowedSpaces: "resource:///modules/ExtensionToolbarButtons.jsm",
+});
+
+/**
+ * Maps XUL toolbar item IDs to unified toolbar item IDs. If null, the item is
+ * not available in the unified toolbar.
+ */
+const MIGRATION_MAP = {
+ separator: null,
+ spacer: "spacer",
+ spring: "spacer",
+ "button-getmsg": "get-messages",
+ "button-newmsg": "write-message",
+ "button-reply": "reply",
+ "button-replyall": "reply-all",
+ "button-replylist": "reply-list",
+ "button-forward": "forward-inline",
+ "button-redirect": "redirect",
+ "button-file": "move-to",
+ "button-archive": "archive",
+ "button-showconversation": "conversation",
+ "button-goback": "go-back",
+ "button-goforward": "go-forward",
+ "button-previous": "previous-unread",
+ "button-previousMsg": "previous",
+ "button-next": "next-unread",
+ "button-nextMsg": "next",
+ "button-junk": "junk",
+ "button-delete": "delete",
+ "button-print": "print",
+ "button-mark": "mark-as",
+ "button-tag": "tag-message",
+ "qfb-show-filter-bar": "quick-filter-bar",
+ "button-address": "address-book",
+ "button-chat": "chat",
+ "throbber-box": "throbber",
+ "button-stop": "stop",
+ "button-compact": "compact",
+ "folder-location-container": "folder-location",
+ "mailviews-container": "view-picker",
+ "button-addons": "add-ons-and-themes",
+ "button-appmenu": null,
+ "gloda-search": "search-bar",
+ "lightning-button-calendar": "calendar",
+ "lightning-button-tasks": "tasks",
+ extractEventButton: "add-as-event",
+ extractTaskButton: "add-as-task",
+ "menubar-items": null,
+ "calendar-synchronize-button": "synchronize",
+ "calendar-newevent-button": "new-event",
+ "calendar-newtask-button": "new-task",
+ "calendar-goto-today-button": "go-to-today",
+ "calendar-edit-button": "edit-event",
+ "calendar-delete-button": "delete-event",
+ "calendar-print-button": "print-event",
+ "calendar-unifinder-button": "unifinder",
+ "calendar-appmenu-button": null,
+ "task-synchronize-button": "synchronize",
+ "task-newevent-button": "new-event",
+ "task-newtask-button": "new-task",
+ "task-edit-button": "edit-event",
+ "task-delete-button": "delete-event",
+ "task-print-button": "print-event",
+ "task-appmenu-button": null,
+};
+
+/**
+ * Maps space names to the ID of the toolbar in the messenger window.
+ */
+const TOOLBAR_FOR_SPACE = {
+ mail: "mail-bar3",
+ calendar: "calendar-toolbar2",
+ tasks: "task-toolbar2",
+};
+
+/**
+ * XUL toolbars store a special value when there are no items in the toolbar.
+ */
+const EMPTY_SET = "__empty";
+/**
+ * Map from the XUL toolbar id to its default set. Since toolbars we're
+ * migrating were removed from the DOM. The value should be the value of the
+ * defaultset attribute of the respective element in the markup.
+ *
+ * @type {{[string]: string}}
+ */
+const XUL_TOOLBAR_DEFAULT_SET = {
+ "mail-bar3":
+ AppConstants.platform == "macosx"
+ ? "button-getmsg,button-newmsg,button-tag,qfb-show-filter-bar,spring,gloda-search,button-appmenu"
+ : "button-getmsg,button-newmsg,separator,button-tag,qfb-show-filter-bar,spring,gloda-search,button-appmenu",
+ "tabbar-toolbar": "",
+ "toolbar-menubar": "menubar-items,spring",
+ "calendar-toolbar2":
+ "calendar-synchronize-button,calendar-newevent-button,calendar-newtask-button,calendar-edit-button,calendar-delete-button,spring,calendar-appmenu-button",
+ "task-toolbar2":
+ "task-synchronize-button,task-newevent-button,task-newtask-button,task-edit-button,task-delete-button,spring,task-appmenu-button",
+};
+const MESSENGER_WINDOW = "chrome://messenger/content/messenger.xhtml";
+const EXTENSION_WIDGET_SUFFIX = "-browserAction-toolbarbutton";
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "extensionIds",
+ "extensions.webextensions.uuids",
+ "{}",
+ null,
+ value => Object.keys(JSON.parse(value))
+);
+
+/**
+ * Get the extension ID from a XUL toolbar button ID of an extension.
+ *
+ * @param {string} buttonId - ID of the XUL toolbar button.
+ * @returns {?string} ID of the extension the button belonged to.
+ */
+function getExtensionIdFromExtensionButton(buttonId) {
+ const widgetId = buttonId.slice(0, -EXTENSION_WIDGET_SUFFIX.length);
+ return lazy.extensionIds.find(
+ extensionId => lazy.ExtensionCommon.makeWidgetId(extensionId) === widgetId
+ );
+}
+
+/**
+ * Convert the string contents of an old toolbar *set attribute to an array of
+ * item IDs.
+ *
+ * @param {string} setString - Contents of the set attribute.
+ * @returns {string[]} Array of items in the set.
+ */
+function toolbarSetAttributeToArray(setString) {
+ if (!setString || setString === EMPTY_SET) {
+ return [];
+ }
+ return setString.split(",").filter(Boolean);
+}
+
+/**
+ * Get the default set (without extensions) of a XUL toolbar.
+ *
+ * @param {string} toolbarId - ID of the XUL toolbar element.
+ * @param {string} window - URI of the window the toolbar is in.
+ * @returns {string} defaultset attribute of the given XUL toolbar.
+ */
+function getOldToolbarDefaultContents(toolbarId, window = MESSENGER_WINDOW) {
+ let setString = Services.xulStore.getValue(window, toolbarId, "defaultset");
+ if (!setString) {
+ setString = XUL_TOOLBAR_DEFAULT_SET[toolbarId];
+ }
+ return setString;
+}
+
+/**
+ * Get the items in a XUL toolbar area. Will return defaults if the area is not
+ * customized.
+ *
+ * @param {string} toolbarId - ID of the XUL toolbar element.
+ * @param {string} window - URI of the window the toolbar is in.
+ * @returns {string[]} Item IDs in the given XUL toolbar.
+ */
+function getOldToolbarContents(toolbarId, window = MESSENGER_WINDOW) {
+ let setString = Services.xulStore.getValue(window, toolbarId, "currentset");
+ if (!setString) {
+ setString = getOldToolbarDefaultContents(toolbarId, window);
+ }
+ return toolbarSetAttributeToArray(setString);
+}
+
+/**
+ * Converts XUL toolbar item IDs to unified toolbar item IDs, filtering out
+ * items that are not supported in the unified toolbar.
+ *
+ * @param {string[]} items - XUL toolbar item IDs to convert.
+ * @returns {string[]} Unified toolbar item IDs.
+ */
+function convertContents(items) {
+ return items
+ .map(itemId => {
+ if (MIGRATION_MAP.hasOwnProperty(itemId)) {
+ return MIGRATION_MAP[itemId];
+ }
+ if (itemId.endsWith(EXTENSION_WIDGET_SUFFIX)) {
+ const extensionId = getExtensionIdFromExtensionButton(itemId);
+ if (extensionId) {
+ return `${EXTENSION_PREFIX}${extensionId}`;
+ }
+ }
+ return null;
+ })
+ .filter(Boolean);
+}
+
+/**
+ * Get the unified toolbar item IDs for items that were in the tab bar and the
+ * menu bar areas.
+ *
+ * @returns {string[]} Item IDs that were available in any tab in the XUL
+ * toolbars.
+ */
+function getGlobalItems() {
+ const tabsContent = convertContents(getOldToolbarContents("tabbar-toolbar"));
+ const menubarContent = convertContents(
+ getOldToolbarContents("toolbar-menubar")
+ );
+ return [...menubarContent, ...tabsContent];
+}
+
+/**
+ * Converts the items in the old xul toolbar of a given space and the tab bar
+ * and menu bar areas to unified toolbar item IDs.
+ *
+ * Filters out any items not available and items that appear multiple times, if
+ * they can't be repeated. The first instance is kept.
+ *
+ * If there is no old toolbar for the given space, only the global items are
+ * returned.
+ *
+ * @param {string} space - Name of the space to get the items for.
+ * @returns {string[]} Unified toolbar item IDs based on the old contents of the
+ * xul toolbar of the space.
+ */
+function getItemsForSpace(space) {
+ let spaceContent = [];
+ if (TOOLBAR_FOR_SPACE.hasOwnProperty(space)) {
+ spaceContent = convertContents(
+ getOldToolbarContents(TOOLBAR_FOR_SPACE[space])
+ );
+ } else {
+ spaceContent = getDefaultItemIdsForSpace(space);
+ }
+ const newContents = [...spaceContent, ...getGlobalItems()];
+ const availableItems = getAvailableItemIdsForSpace(space, true).concat(
+ lazy.extensionIds.map(id => `${EXTENSION_PREFIX}${id}`)
+ );
+ const encounteredItems = new Set();
+ const finalItems = newContents.filter((itemId, index, items) => {
+ if (
+ (encounteredItems.has(itemId) &&
+ !MULTIPLE_ALLOWED_ITEM_IDS.has(itemId)) ||
+ !availableItems.includes(itemId) ||
+ (itemId === "spacer" && index > 0 && items[index - 1] === itemId)
+ ) {
+ return false;
+ }
+ encounteredItems.add(itemId);
+ return true;
+ });
+ return finalItems;
+}
+
+/**
+ * Convert the persisted extensions from the old extensionset to the new space
+ * specific store for extensions.
+ *
+ * @param {string} space - Name of the migrated space.
+ */
+function convertExtensionState(space) {
+ if (
+ !Services.xulStore.hasValue(
+ MESSENGER_WINDOW,
+ TOOLBAR_FOR_SPACE[space],
+ "extensionset"
+ )
+ ) {
+ return;
+ }
+ const extensionSet = Services.xulStore
+ .getValue(MESSENGER_WINDOW, TOOLBAR_FOR_SPACE[space], "extensionset")
+ .split(",")
+ .filter(Boolean);
+ const extensionsInExtensionSet = extensionSet.map(buttonId =>
+ getExtensionIdFromExtensionButton(buttonId)
+ );
+ const cachedAllowedSpaces = lazy.getCachedAllowedSpaces();
+ for (const extensionId of extensionsInExtensionSet) {
+ const allowedSpaces = cachedAllowedSpaces.get(extensionId) ?? [];
+ if (!allowedSpaces.includes(space)) {
+ allowedSpaces.push(space);
+ }
+ cachedAllowedSpaces.set(extensionId, allowedSpaces);
+ }
+ lazy.setCachedAllowedSpaces(cachedAllowedSpaces);
+}
+
+/**
+ * Check if the XUL toolbar matches the default state.
+ *
+ * @param {string} toolbarId - ID of the old XUL toolbar element to check the
+ * state of.
+ * @returns {boolean} If the toolbar with the given ID has a currentset matching
+ * the default state for that toolbar.
+ */
+function oldToolbarContainsDefaultItems(toolbarId) {
+ // Fast path: if there is no current set, the contents of the toolbar were
+ // never modified.
+ if (!Services.xulStore.hasValue(MESSENGER_WINDOW, toolbarId, "currentset")) {
+ return true;
+ }
+ const toolbarContents = getOldToolbarContents(toolbarId);
+ let defaultContents = toolbarSetAttributeToArray(
+ getOldToolbarDefaultContents(toolbarId)
+ );
+ const extensionContents = toolbarSetAttributeToArray(
+ Services.xulStore.getValue(MESSENGER_WINDOW, toolbarId, "extensionset")
+ );
+ // Extensions are inserted before the appmenu button, which is usually at the
+ // end of the default set.
+ if (extensionContents.length) {
+ const appmenuIndex = defaultContents.findIndex(
+ itemId => itemId === "button-appmenu"
+ );
+ if (appmenuIndex !== -1) {
+ defaultContents.splice(appmenuIndex, 0, ...extensionContents);
+ } else {
+ defaultContents = defaultContents.concat(extensionContents);
+ }
+ }
+ return (
+ toolbarContents.length === defaultContents.length &&
+ toolbarContents.every((itemId, index) => itemId === defaultContents[index])
+ );
+}
+
+/**
+ * Check if the XUL toolbar customization state is equivalent to its default set
+ * for a given space.
+ *
+ * @param {string} space - Name of the space to check the default set for.
+ * @returns {boolean} If the state of the old XUL toolbars matches the default
+ * set for that space. True if we don't know any toolbar for the given space.
+ */
+function stateMatchesDefault(space) {
+ if (!TOOLBAR_FOR_SPACE.hasOwnProperty(space)) {
+ return true;
+ }
+ if (!oldToolbarContainsDefaultItems(TOOLBAR_FOR_SPACE[space])) {
+ return false;
+ }
+ if (space === "mail") {
+ if (!oldToolbarContainsDefaultItems("tabbar-toolbar")) {
+ return false;
+ }
+ if (!oldToolbarContainsDefaultItems("toolbar-menubar")) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * Remove all the persisted state of a XUL toolbar from the XUL store.
+ *
+ * @param {string} toolbarId - Element ID of the XUL toolbar to clear the state
+ * of.
+ */
+export function clearXULToolbarState(toolbarId) {
+ Services.xulStore.removeValue(MESSENGER_WINDOW, toolbarId, "currentset");
+ Services.xulStore.removeValue(MESSENGER_WINDOW, toolbarId, "defaultset");
+ Services.xulStore.removeValue(MESSENGER_WINDOW, toolbarId, "extensionset");
+}
+
+/**
+ * Migrate the old xul toolbar contents for a given space to the unified toolbar
+ * if the unified toolbar has not yet been customized.
+ *
+ * Adds both the contents of the space specific toolbar and the tab bar and menu
+ * bar areas to the unified toolbar, if the items are available.
+ *
+ * When the migration is complete, the old XUL store values for the XUL toolbar
+ * area are deleted.
+ *
+ * @param {string} space - Name of the space to migrate.
+ */
+export function migrateToolbarForSpace(space) {
+ const state = getState();
+ // If the mail toolbar areas are all in their default state, we don't want to
+ // migrate their contents.
+ const mailToolbarInDefaultState =
+ space === "mail" && stateMatchesDefault(space);
+ // Don't migrate contents if the state of the space is already customized.
+ if (state[space] || mailToolbarInDefaultState) {
+ if (mailToolbarInDefaultState && TOOLBAR_FOR_SPACE.hasOwnProperty(space)) {
+ clearXULToolbarState(TOOLBAR_FOR_SPACE[space]);
+ }
+ return;
+ }
+ state[space] = getItemsForSpace(space);
+ storeState(state);
+ if (TOOLBAR_FOR_SPACE.hasOwnProperty(space)) {
+ convertExtensionState(space);
+ // Remove all the state for the old toolbar of the space.
+ clearXULToolbarState(TOOLBAR_FOR_SPACE[space]);
+ }
+}