diff options
Diffstat (limited to '')
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]); + } +} |