Bug 1499617 - Folder tab (3-pane) WebExtensions API; r=mkmelin

This commit is contained in:
Geoff Lankow 2018-12-30 20:32:40 +13:00
Родитель 71005c0395
Коммит d080e8cc4b
8 изменённых файлов: 743 добавлений и 2 удалений

Просмотреть файл

@ -50,6 +50,15 @@
"scopes": ["addon_parent"],
"manifest": ["legacy"]
},
"mailTabs": {
"url": "chrome://messenger/content/parent/ext-mailTabs.js",
"schema": "chrome://messenger/content/schemas/mailTabs.json",
"scopes": ["addon_parent"],
"manifest": ["mailTabs"],
"paths": [
["mailTabs"]
]
},
"pkcs11": {
"url": "chrome://messenger/content/parent/ext-pkcs11.js",
"schema": "chrome://messenger/content/schemas/pkcs11.json",

Просмотреть файл

@ -16,6 +16,7 @@ messenger.jar:
content/messenger/parent/ext-composeAction.js (parent/ext-composeAction.js)
content/messenger/parent/ext-legacy.js (parent/ext-legacy.js)
content/messenger/parent/ext-mail.js (parent/ext-mail.js)
content/messenger/parent/ext-mailTabs.js (parent/ext-mailTabs.js)
content/messenger/parent/ext-pkcs11.js (../../../../browser/components/extensions/parent/ext-pkcs11.js)
content/messenger/parent/ext-tabs.js (parent/ext-tabs.js)
content/messenger/parent/ext-windows.js (parent/ext-windows.js)
@ -26,6 +27,7 @@ messenger.jar:
content/messenger/schemas/commands.json (schemas/commands.json)
content/messenger/schemas/composeAction.json (schemas/composeAction.json)
content/messenger/schemas/legacy.json (schemas/legacy.json)
content/messenger/schemas/mailTabs.json (schemas/mailTabs.json)
content/messenger/schemas/pkcs11.json (../../../../browser/components/extensions/schemas/pkcs11.json)
content/messenger/schemas/tabs.json (schemas/tabs.json)
content/messenger/schemas/windows.json (schemas/windows.json)

Просмотреть файл

@ -37,6 +37,10 @@ module.exports = {
"Tab": true,
"Window": true,
"WindowEventManager": true,
"convertFolder": true,
"convertMessage": true,
"folderPathToURI": true,
"folderURIToPath": true,
"getTabBrowser": true,
"makeWidgetId": true,
"tabGetSender": true,

Просмотреть файл

@ -2,6 +2,8 @@
* 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/. */
ChromeUtils.defineModuleGetter(this, "MailServices", "resource:///modules/MailServices.jsm");
var {
ExtensionError,
} = ExtensionUtils;
@ -504,9 +506,21 @@ Object.assign(global, { tabTracker, windowTracker });
* Extension-specific wrapper around a Thunderbird tab.
*/
class Tab extends TabBase {
/** Removes some useless properties from a tab object. */
/** Returns true if this tab is a 3-pane tab. */
get isMail3Pane() {
return this.nativeTab.mode.type == "folder";
}
/** Overrides the matches function to enable querying for 3-pane tabs. */
matches(queryInfo, context) {
let result = super.matches(queryInfo, context);
return result && (!queryInfo.isMail3Pane || this.isMail3Pane);
}
/** Adds the isMail3Pane property and removes some useless properties from a tab object. */
convert(fallback) {
let result = super.convert(fallback);
result.isMail3Pane = this.isMail3Pane;
// These properties are not useful to Thunderbird extensions and are not returned.
for (let key of [
@ -988,3 +1002,79 @@ extensions.on("startup", (type, extension) => { // eslint-disable-line mozilla/b
defineLazyGetter(extension, "windowManager",
() => new WindowManager(extension));
});
/**
* The following functions turn nsIMsgFolder references into more human-friendly forms.
* A folder can be referenced with the account key, and the path to the folder in that account.
*/
/**
* Convert a folder URI to a human-friendly path.
* @return {String}
*/
function folderURIToPath(uri) {
let path = Services.io.newURI(uri).filePath;
return path.split("/").map(decodeURIComponent).join("/");
}
/**
* Convert a human-friendly path to a folder URI. This function does not assume that the
* folder referenced exists.
* @return {String}
*/
function folderPathToURI(accountId, path) {
let rootURI = MailServices.accounts.getAccount(accountId).incomingServer.rootFolder.URI;
if (path == "/") {
return rootURI;
}
return rootURI + path.split("/").map(encodeURIComponent).join("/");
}
/**
* Converts an nsIMsgFolder to a simple object for use in messages.
* @return {Object}
*/
function convertFolder(folder, accountId) {
if (!folder) {
return null;
}
if (!accountId) {
let server = folder.server;
let account = MailServices.accounts.FindAccountForServer(server);
accountId = account.key;
}
return {
accountId,
name: folder.prettyName,
path: folderURIToPath(folder.URI),
};
}
/**
* Converts an nsIMsgHdr to a simle object for use in messages.
* This function WILL change as the API develops.
* @return {Object}
*/
function convertMessage(msgHdr) {
if (!msgHdr) {
return null;
}
return {
messageId: msgHdr.messageId,
read: msgHdr.isRead,
flagged: msgHdr.isFlagged,
ccList: msgHdr.ccList,
bccList: msgHdr.bccList,
author: msgHdr.mime2DecodedAuthor,
subject: msgHdr.mime2DecodedSubject,
recipients: msgHdr.mime2DecodedRecipients,
};
}
Object.assign(global, {
convertFolder,
convertMessage,
folderPathToURI,
folderURIToPath,
});

Просмотреть файл

@ -0,0 +1,344 @@
/* 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/. */
ChromeUtils.defineModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(this, "MailServices", "resource:///modules/MailServices.jsm");
ChromeUtils.defineModuleGetter(this, "QuickFilterManager",
"resource:///modules/QuickFilterManager.jsm");
const LAYOUTS = ["standard", "wide", "vertical"];
// From nsIMsgDBView.idl
const SORT_TYPE_MAP = new Map(
Object.keys(Ci.nsMsgViewSortType).map(key => [Ci.nsMsgViewSortType[key], key])
);
const SORT_ORDER_MAP = new Map(
Object.keys(Ci.nsMsgViewSortOrder).map(key => [Ci.nsMsgViewSortOrder[key], key])
);
/**
* Converts a mail tab to a simle object for use in messages.
* @return {Object}
*/
function convertMailTab(tab, context) {
let mailTabObject = {
id: tab.id,
windowId: tab.windowId,
active: tab.active,
sortType: null,
sortOrder: null,
layout: LAYOUTS[Services.prefs.getIntPref("mail.pane_config.dynamic")],
folderPaneVisible: null,
messagePaneVisible: null,
};
let nativeTab = tab.nativeTab;
let { folderDisplay } = nativeTab;
if (folderDisplay.view.displayedFolder) {
let { folderPaneVisible, messagePaneVisible } = nativeTab.mode.persistTab(nativeTab);
mailTabObject.sortType = SORT_TYPE_MAP.get(folderDisplay.view.primarySortType);
mailTabObject.sortOrder = SORT_ORDER_MAP.get(folderDisplay.view.primarySortOrder);
mailTabObject.folderPaneVisible = folderPaneVisible;
mailTabObject.messagePaneVisible = messagePaneVisible;
}
if (context.extension.hasPermission("accountsRead")) {
mailTabObject.displayedFolder = convertFolder(folderDisplay.displayedFolder);
}
return mailTabObject;
}
/**
* Listens for changes in the UI to fire events.
*/
var uiListener = new class extends EventEmitter {
constructor() {
super();
this.listenerCount = 0;
this.handleSelect = this.handleSelect.bind(this);
this.lastSelected = new WeakMap();
}
handleSelect(event) {
let tab = tabTracker.activeTab;
if (event.target.id == "folderTree") {
let folder = tab.folderDisplay.displayedFolder;
if (this.lastSelected.get(tab) == folder) {
return;
}
this.lastSelected.set(tab, folder);
this.emit("folder-changed", tab, folder);
return;
}
if (event.target.id == "threadTree") {
this.emit("messages-changed", tab, tab.folderDisplay.view.dbView.getSelectedMsgHdrs());
}
}
addListenersToWindow(window) {
window.addEventListener("select", uiListener.handleSelect);
}
removeListenersFromWindow(window) {
window.removeEventListener("select", uiListener.handleSelect);
}
incrementListeners() {
this.listenerCount++;
if (this.listenerCount == 1) {
for (let window of windowTracker.browserWindows()) {
this.addListenersToWindow(window);
}
windowTracker.addOpenListener(this.addListenersToWindow);
}
}
decrementListeners() {
this.listenerCount--;
if (this.listenerCount == 0) {
for (let window of windowTracker.browserWindows()) {
this.removeListenersFromWindow(window);
}
windowTracker.removeOpenListener(this.addListenersToWindow);
this.lastSelected = new WeakMap();
}
}
};
class PermissionedEventManager extends EventManager {
constructor({ permission, context, name, register }) {
super({ context, name, register });
this.permission = permission;
}
addListener(callback) {
let { extension } = this.context;
if (!extension.hasPermission(this.permission)) {
throw new ExtensionError(
`The "${this.permission}" permission is required to use ${this.name}.`
);
}
return super.addListener(callback);
}
}
this.mailTabs = class extends ExtensionAPI {
getAPI(context) {
let { extension } = context;
let { tabManager } = extension;
/**
* Gets the tab for the given tab id, or the active tab if the id is null.
*
* @param {?Integer} tabId The tab id to get
* @return {Tab} The matching tab, or the active tab
*/
function getTabOrActive(tabId) {
let tab;
if (tabId) {
tab = tabManager.get(tabId);
} else {
tab = tabManager.wrapTab(tabTracker.activeTab);
tabId = tab.id;
}
if (tab && tab.isMail3Pane) {
return tab;
}
throw new ExtensionError(`Invalid mail tab ID: ${tabId}`);
}
return {
mailTabs: {
async getAll() {
return Array.from(tabManager.query({
// All of these are needed for tabManager to return every tab we want.
"currentWindow": null,
"index": null,
"isMail3Pane": true,
"lastFocusedWindow": null,
"screen": null,
"windowId": null,
"windowType": null,
}, context), (tab) => convertMailTab(tab, context));
},
async getCurrent() {
let tab = tabManager.wrapTab(tabTracker.activeTab);
if (!tab || !tab.isMail3Pane) {
return null;
}
return convertMailTab(tab, context);
},
async update(tabId, args) {
let tab = getTabOrActive(tabId);
let window = tab.window;
let {
displayedFolder,
layout,
folderPaneVisible,
messagePaneVisible,
sortOrder,
sortType,
} = args;
if (displayedFolder && extension.hasPermission("accountsRead")) {
let uri = folderPathToURI(displayedFolder.accountId, displayedFolder.path);
if (tab.active) {
let treeView = Cu.getGlobalForObject(tab.nativeTab).gFolderTreeView;
let folder = MailServices.folderLookup.getFolderForURL(uri);
if (folder) {
treeView.selectFolder(folder);
} else {
throw new ExtensionError(
`Folder "${displayedFolder.path}" for account ` +
`"${displayedFolder.accountId}" not found.`
);
}
} else {
tab.nativeTab.folderDisplay.showFolderUri(uri);
}
}
if (sortType && sortType in Ci.nsMsgViewSortType &&
sortOrder && sortOrder in Ci.nsMsgViewSortOrder) {
tab.nativeTab.folderDisplay.view.sort(Ci.nsMsgViewSortType[sortType],
Ci.nsMsgViewSortOrder[sortOrder]);
}
// Layout applies to all folder tabs.
if (layout) {
Services.prefs.setIntPref("mail.pane_config.dynamic", LAYOUTS.indexOf(layout));
}
if (typeof folderPaneVisible == "boolean") {
if (tab.active) {
let document = window.document;
let folderPaneSplitter = document.getElementById("folderpane_splitter");
folderPaneSplitter.setAttribute("state", folderPaneVisible ? "open" : "collapsed");
} else {
tab.nativeTab.folderDisplay.folderPaneVisible = folderPaneVisible;
}
}
if (typeof messagePaneVisible == "boolean") {
if (tab.active) {
if (messagePaneVisible == window.IsMessagePaneCollapsed()) {
window.MsgToggleMessagePane();
}
} else {
tab.nativeTab.messageDisplay._visible = messagePaneVisible;
if (!messagePaneVisible) {
// Prevent the messagePane from showing if a message is selected.
tab.nativeTab.folderDisplay._aboutToSelectMessage = true;
}
}
}
},
async getSelectedMessages(tabId) {
if (!extension.hasPermission("messagesRead")) {
throw new ExtensionError(
`The "messagesRead" permission is required to use mailTabs.getSelectedMessages.`
);
}
let tab = getTabOrActive(tabId);
let { folderDisplay } = tab.nativeTab;
return [...folderDisplay.view.dbView.getSelectedMsgHdrs()].map(convertMessage);
},
async setQuickFilter(tabId, state) {
let tab = getTabOrActive(tabId);
let nativeTab = tab.nativeTab;
let window = Cu.getGlobalForObject(nativeTab);
let filterer;
if (tab.active) {
filterer = window.QuickFilterBarMuxer.activeFilterer;
} else {
filterer = nativeTab._ext.quickFilter;
}
filterer.clear();
filterer.visible = (state.show !== false);
for (let s of ["unread", "starred", "contact", "attachment"]) {
let key = (s == "contact") ? "addrBook" : s;
let value = state[s];
if (value === null) {
delete filterer.filterValues[key];
} else {
filterer.filterValues[key] = value;
}
}
if (state.tags) {
filterer.filterValues.tags = {
mode: "OR",
tags: {},
};
for (let tag of MailServices.tags.getAllTags({})) {
filterer.filterValues.tags[tag.key] = null;
}
if (typeof state.tags == "object") {
filterer.filterValues.tags.mode = (state.tags.mode == "any") ? "OR" : "AND";
for (let [key, value] of Object.entries(state.tags.tags)) {
filterer.filterValues.tags.tags[key] = value;
}
}
}
if (state.text) {
filterer.filterValues.text = {
states: {
recipients: state.text.recipients || false,
sender: state.text.sender || false,
subject: state.text.subject || false,
body: state.text.body || false,
},
text: state.text.text,
};
}
if (tab.active) {
window.QuickFilterBarMuxer.deferredUpdateSearch();
window.QuickFilterBarMuxer.reflectFiltererState(filterer, window.gFolderDisplay);
}
// Inactive tabs are updated when they become active, except the search doesn't. :(
},
onDisplayedFolderChanged: new PermissionedEventManager({
permission: "accountsRead",
context,
name: "mailTabs.onDisplayedFolderChanged",
register: (fire) => {
let listener = (event, tab, folder) => {
fire.sync(tabTracker.getId(tab), convertFolder(folder));
};
uiListener.on("folder-changed", listener);
uiListener.incrementListeners();
return () => {
uiListener.off("folder-changed", listener);
uiListener.decrementListeners();
};
},
}).api(),
onSelectedMessagesChanged: new PermissionedEventManager({
permission: "messagesRead",
context,
name: "mailTabs.onSelectedMessagesChanged",
register: (fire) => {
let listener = (event, tab, messages) => {
fire.sync(tabTracker.getId(tab), messages.map(convertMessage));
};
uiListener.on("messages-changed", listener);
uiListener.incrementListeners();
return () => {
uiListener.off("messages-changed", listener);
uiListener.decrementListeners();
};
},
}).api(),
},
};
}
};

Просмотреть файл

@ -0,0 +1,282 @@
[
{
"namespace": "manifest",
"types": [
{
"$extend": "OptionalPermission",
"choices": [
{
"type": "string",
"enum": [
"mailTabs"
]
}
]
}
]
},
{
"namespace": "mailTabs",
"permissions": [
"mailTabs"
],
"types": [
{
"id": "QuickFilterTagsDetail",
"type": "object",
"properties": {
"tags": {
"type": "object",
"description": "Object keys are tags to filter on, values are <code>true</code> if the message must have the tag, or <code>false</code> if it must not have the tag. For a list of available tags, call the :ref:`messages.listTags` method.",
"patternProperties": {
".*": {
"type": "boolean"
}
}
},
"mode": {
"type": "string",
"description": "Whether all of the tag filters must apply, or any of them.",
"enum": [
"all",
"any"
]
}
}
},
{
"id": "QuickFilterTextDetail",
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "String to match against the <var>recipients</var>, <var>sender</var>, <var>subject</var>, or <var>body</var>."
},
"recipients": {
"type": "boolean",
"description": "Shows messages where <var>text</var> matches the recipients.",
"optional": true
},
"sender": {
"type": "boolean",
"description": "Shows messages where <var>text</var> matches the sender.",
"optional": true
},
"subject": {
"type": "boolean",
"description": "Shows messages where <var>text</var> matches the subject.",
"optional": true
},
"body": {
"type": "boolean",
"description": "Shows messages where <var>text</var> matches the message body.",
"optional": true
}
}
}
],
"functions": [
{
"name": "getAll",
"type": "function",
"description": "Returns an array of all mail tabs in all windows.",
"async": true,
"parameters": []
},
{
"name": "getCurrent",
"type": "function",
"description": "Returns the current mail tab in the most recent window, or throws an exception if the current tab is not a mail tab.",
"async": true,
"parameters": []
},
{
"name": "update",
"type": "function",
"description": "Modifies the properties of a mail tab. Properties that are not specified in <var>updateProperties</var> are not modified.",
"async": true,
"parameters": [
{
"name": "tabId",
"type": "integer",
"description": "Defaults to the selected tab of the current window.",
"optional": true,
"minimum": 1
},
{
"name": "updateProperties",
"type": "object",
"properties": {
"displayedFolder": {
"type": "object",
"description": "Sets the folder displayed in the tab. The extension must have an accounts permission to do this.",
"optional": true,
"properties": {
"accountId": {
"type": "string"
},
"name": {
"type": "string",
"optional": true
},
"path": {
"type": "string"
}
}
},
"sortType": {
"type": "string",
"description": "Sorts the list of messages. <var>sortOrder</var> must also be given.",
"optional": true,
"enum": [
"byNone",
"byDate",
"bySubject",
"byAuthor",
"byId",
"byThread",
"byPriority",
"byStatus",
"bySize",
"byFlagged",
"byUnread",
"byRecipient",
"byLocation",
"byTags",
"byJunkStatus",
"byAttachments",
"byAccount",
"byCustom",
"byReceived",
"byCorrespondent"
]
},
"sortOrder": {
"type": "string",
"description": "Sorts the list of messages. <var>sortType</var> must also be given.",
"optional": true,
"enum": [
"none",
"ascending",
"descending"
]
},
"layout": {
"type": "string",
"description": "Sets the arrangement of the folder pane, message list pane, and message display pane. Note that setting this applies it to all mail tabs.",
"optional": true,
"enum": [
"standard",
"wide",
"vertical"
]
},
"folderPaneVisible": {
"type": "boolean",
"description": "Shows or hides the folder pane.",
"optional": true
},
"messagePaneVisible": {
"type": "boolean",
"description": "Shows or hides the message display pane.",
"optional": true
}
}
}
]
},
{
"name": "getSelectedMessages",
"type": "function",
"description": "Lists the selected messages in the current folder. A messages permission is required to do this.",
"async": true,
"parameters": [
{
"name": "tabId",
"type": "integer",
"description": "Defaults to the selected tab of the current window.",
"optional": true,
"minimum": 1
}
]
},
{
"name": "setQuickFilter",
"type": "function",
"description": "Sets the Quick Filter user interface based on the options specified.",
"async": true,
"parameters": [
{
"name": "tabId",
"type": "integer",
"description": "Defaults to the selected tab of the current window.",
"optional": true,
"minimum": 1
},
{
"name": "properties",
"type": "object",
"properties": {
"show": {
"type": "boolean",
"description": "Shows or hides the Quick Filter bar.",
"optional": true
},
"unread": {
"type": "boolean",
"description": "Shows only unread messages.",
"optional": true
},
"starred": {
"type": "boolean",
"description": "Shows only starred messages.",
"optional": true
},
"contact": {
"type": "boolean",
"description": "Shows only messages from people in the address book.",
"optional": true
},
"tags": {
"optional": true,
"choices": [
{
"type": "boolean"
},
{
"$ref": "QuickFilterTagsDetail"
}
],
"description": "Shows only messages with tags on them."
},
"attachment": {
"type": "boolean",
"description": "Shows only messages with attachments.",
"optional": true
},
"text": {
"$ref": "QuickFilterTextDetail",
"description": "Shows only messages matching the supplied text.",
"optional": true
}
}
}
]
}
],
"events": [
{
"name": "onDisplayedFolderChanged",
"type": "function",
"description": "Fired when the displayed folder changes in any mail tab.",
"parameters": []
},
{
"name": "onSelectedMessagesChanged",
"type": "function",
"description": "Fired when the selected messages change in any mail tab.",
"parameters": []
}
]
}
]

Просмотреть файл

@ -38,7 +38,8 @@
"favIconUrl": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL of the tab's favicon. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission. It may also be an empty string if the tab is loading."},
"status": {"type": "string", "optional": true, "description": "Either <em>loading</em> or <em>complete</em>."},
"width": {"type": "integer", "optional": true, "description": "The width of the tab in pixels."},
"height": {"type": "integer", "optional": true, "description": "The height of the tab in pixels."}
"height": {"type": "integer", "optional": true, "description": "The height of the tab in pixels."},
"isMail3Pane": {"type": "boolean", "optional": true, "description": "Whether the tab is a 3-pane tab."}
}
},
{
@ -226,6 +227,11 @@
"type": "object",
"name": "queryInfo",
"properties": {
"isMail3Pane": {
"type": "boolean",
"optional": true,
"description": "Whether the tab is a Thunderbird 3-pane tab."
},
"active": {
"type": "boolean",
"optional": true,

Просмотреть файл

@ -71,3 +71,7 @@ XPCOMUtils.defineLazyServiceGetter(MailServices, "junk",
XPCOMUtils.defineLazyServiceGetter(MailServices, "newMailNotification",
"@mozilla.org/newMailNotificationService;1",
"mozINewMailNotificationService");
XPCOMUtils.defineLazyServiceGetter(MailServices, "folderLookup",
"@mozilla.org/mail/folder-lookup;1",
"nsIFolderLookupService");