summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/extensions/parent/ext-messageDisplay.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/extensions/parent/ext-messageDisplay.js')
-rw-r--r--comm/mail/components/extensions/parent/ext-messageDisplay.js348
1 files changed, 348 insertions, 0 deletions
diff --git a/comm/mail/components/extensions/parent/ext-messageDisplay.js b/comm/mail/components/extensions/parent/ext-messageDisplay.js
new file mode 100644
index 0000000000..98ba2dc75c
--- /dev/null
+++ b/comm/mail/components/extensions/parent/ext-messageDisplay.js
@@ -0,0 +1,348 @@
+/* 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/. */
+
+var { MailConsts } = ChromeUtils.import("resource:///modules/MailConsts.jsm");
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+/**
+ * Returns the currently displayed messages in the given tab.
+ *
+ * @param {Tab} tab
+ * @returns {nsIMsgHdr[]} Array of nsIMsgHdr
+ */
+function getDisplayedMessages(tab) {
+ let nativeTab = tab.nativeTab;
+ if (tab instanceof TabmailTab) {
+ if (nativeTab.mode.name == "mail3PaneTab") {
+ return nativeTab.chromeBrowser.contentWindow.gDBView.getSelectedMsgHdrs();
+ } else if (nativeTab.mode.name == "mailMessageTab") {
+ return [nativeTab.chromeBrowser.contentWindow.gMessage];
+ }
+ } else if (nativeTab?.messageBrowser) {
+ return [nativeTab.messageBrowser.contentWindow.gMessage];
+ }
+ return [];
+}
+
+/**
+ * Wrapper to convert multiple nsIMsgHdr to MessageHeader objects.
+ *
+ * @param {nsIMsgHdr[]} Array of nsIMsgHdr
+ * @param {ExtensionData} extension
+ * @returns {MessageHeader[]} Array of MessageHeader objects
+ *
+ * @see /mail/components/extensions/schemas/messages.json
+ */
+function convertMessages(messages, extension) {
+ let result = [];
+ for (let msg of messages) {
+ let hdr = convertMessage(msg, extension);
+ if (hdr) {
+ result.push(hdr);
+ }
+ }
+ return result;
+}
+
+/**
+ * Check the users preference on opening new messages in tabs or windows.
+ *
+ * @returns {string} - either "tab" or "window"
+ */
+function getDefaultMessageOpenLocation() {
+ let pref = Services.prefs.getIntPref("mail.openMessageBehavior");
+ return pref == MailConsts.OpenMessageBehavior.NEW_TAB ? "tab" : "window";
+}
+
+/**
+ * Return the msgHdr of the message specified in the properties object. Message
+ * can be specified via properties.headerMessageId or properties.messageId.
+ *
+ * @param {object} properties - @see mail/components/extensions/schemas/messageDisplay.json
+ * @throws ExtensionError if an unknown message has been specified
+ * @returns {nsIMsgHdr} the requested msgHdr
+ */
+function getMsgHdr(properties) {
+ if (properties.headerMessageId) {
+ let msgHdr = MailUtils.getMsgHdrForMsgId(properties.headerMessageId);
+ if (!msgHdr) {
+ throw new ExtensionError(
+ `Unknown or invalid headerMessageId: ${properties.headerMessageId}.`
+ );
+ }
+ return msgHdr;
+ }
+ let msgHdr = messageTracker.getMessage(properties.messageId);
+ if (!msgHdr) {
+ throw new ExtensionError(
+ `Unknown or invalid messageId: ${properties.messageId}.`
+ );
+ }
+ return msgHdr;
+}
+
+this.messageDisplay = class extends ExtensionAPIPersistent {
+ PERSISTENT_EVENTS = {
+ // For primed persistent events (deactivated background), the context is only
+ // available after fire.wakeup() has fulfilled (ensuring the convert() function
+ // has been called).
+
+ onMessageDisplayed({ context, fire }) {
+ const { extension } = this;
+ const { tabManager } = extension;
+ let listener = {
+ async handleEvent(event) {
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ // `event.target` is an about:message window.
+ let nativeTab = event.target.tabOrWindow;
+ let tab = tabManager.wrapTab(nativeTab);
+ let msg = convertMessage(event.detail, extension);
+ fire.async(tab.convert(), msg);
+ },
+ };
+ windowTracker.addListener("MsgLoaded", listener);
+ return {
+ unregister: () => {
+ windowTracker.removeListener("MsgLoaded", listener);
+ },
+ convert(newFire, extContext) {
+ fire = newFire;
+ context = extContext;
+ },
+ };
+ },
+ onMessagesDisplayed({ context, fire }) {
+ const { extension } = this;
+ const { tabManager } = extension;
+ let listener = {
+ async handleEvent(event) {
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ // `event.target` is an about:message or about:3pane window.
+ let nativeTab = event.target.tabOrWindow;
+ let tab = tabManager.wrapTab(nativeTab);
+ let msgs = getDisplayedMessages(tab);
+ fire.async(tab.convert(), convertMessages(msgs, extension));
+ },
+ };
+ windowTracker.addListener("MsgsLoaded", listener);
+ return {
+ unregister: () => {
+ windowTracker.removeListener("MsgsLoaded", listener);
+ },
+ convert(newFire, extContext) {
+ fire = newFire;
+ context = extContext;
+ },
+ };
+ },
+ };
+
+ getAPI(context) {
+ /**
+ * Guard to make sure the API waits until the message tab has been fully loaded,
+ * to cope with tabs.onCreated returning tabs very early.
+ *
+ * @param {integer} tabId
+ * @returns {Tab} the fully loaded message tab identified by the given tabId,
+ * or null, if invalid
+ */
+ async function getMessageDisplayTab(tabId) {
+ let msgContentWindow;
+ let tab = tabManager.get(tabId);
+ if (tab?.type == "mail") {
+ // In about:3pane only the messageBrowser needs to be checked for its
+ // load state. The webBrowser is invalid, the multiMessageBrowser can
+ // bypass.
+ if (!tab.nativeTab.chromeBrowser.contentWindow.webBrowser.hidden) {
+ return null;
+ }
+ if (
+ !tab.nativeTab.chromeBrowser.contentWindow.multiMessageBrowser.hidden
+ ) {
+ return tab;
+ }
+ msgContentWindow =
+ tab.nativeTab.chromeBrowser.contentWindow.messageBrowser
+ .contentWindow;
+ } else if (tab?.type == "messageDisplay") {
+ msgContentWindow =
+ tab instanceof TabmailTab
+ ? tab.nativeTab.chromeBrowser.contentWindow
+ : tab.nativeTab.messageBrowser.contentWindow;
+ } else {
+ return null;
+ }
+
+ // Make sure the content window has been fully loaded.
+ await new Promise(resolve => {
+ if (msgContentWindow.document.readyState == "complete") {
+ resolve();
+ } else {
+ msgContentWindow.addEventListener(
+ "load",
+ () => {
+ resolve();
+ },
+ { once: true }
+ );
+ }
+ });
+
+ // Wait until the message display process has been initiated.
+ await new Promise(resolve => {
+ if (msgContentWindow.msgLoading || msgContentWindow.msgLoaded) {
+ resolve();
+ } else {
+ msgContentWindow.addEventListener(
+ "messageURIChanged",
+ () => {
+ resolve();
+ },
+ { once: true }
+ );
+ }
+ });
+
+ // Wait until the message display process has been finished.
+ await new Promise(resolve => {
+ if (msgContentWindow.msgLoaded) {
+ resolve();
+ } else {
+ msgContentWindow.addEventListener(
+ "MsgLoaded",
+ () => {
+ resolve();
+ },
+ { once: true }
+ );
+ }
+ });
+
+ // If there is no gMessage, then the display has been cleared.
+ return msgContentWindow.gMessage ? tab : null;
+ }
+
+ let { extension } = context;
+ let { tabManager } = extension;
+ return {
+ messageDisplay: {
+ onMessageDisplayed: new EventManager({
+ context,
+ module: "messageDisplay",
+ event: "onMessageDisplayed",
+ extensionApi: this,
+ }).api(),
+ onMessagesDisplayed: new EventManager({
+ context,
+ module: "messageDisplay",
+ event: "onMessagesDisplayed",
+ extensionApi: this,
+ }).api(),
+ async getDisplayedMessage(tabId) {
+ let tab = await getMessageDisplayTab(tabId);
+ if (!tab) {
+ return null;
+ }
+ let messages = getDisplayedMessages(tab);
+ if (messages.length != 1) {
+ return null;
+ }
+ return convertMessage(messages[0], extension);
+ },
+ async getDisplayedMessages(tabId) {
+ let tab = await getMessageDisplayTab(tabId);
+ if (!tab) {
+ return [];
+ }
+ let messages = getDisplayedMessages(tab);
+ return convertMessages(messages, extension);
+ },
+ async open(properties) {
+ if (
+ ["messageId", "headerMessageId", "file"].reduce(
+ (count, value) => (properties[value] ? count + 1 : count),
+ 0
+ ) != 1
+ ) {
+ throw new ExtensionError(
+ "Exactly one of messageId, headerMessageId or file must be specified."
+ );
+ }
+
+ let messageURI;
+ if (properties.file) {
+ let realFile = await getRealFileForFile(properties.file);
+ messageURI = Services.io
+ .newFileURI(realFile)
+ .mutate()
+ .setQuery("type=application/x-message-display")
+ .finalize().spec;
+ } else {
+ let msgHdr = getMsgHdr(properties);
+ if (msgHdr.folder) {
+ messageURI = msgHdr.folder.getUriForMsg(msgHdr);
+ } else {
+ // Add the application/x-message-display type to the url, if missing.
+ // The slash is escaped when setting the type via searchParams, but
+ // core code needs it unescaped.
+ let url = new URL(msgHdr.getStringProperty("dummyMsgUrl"));
+ url.searchParams.delete("type");
+ messageURI = `${url.href}${
+ url.searchParams.toString() ? "&" : "?"
+ }type=application/x-message-display`;
+ }
+ }
+
+ let tab;
+ switch (properties.location || getDefaultMessageOpenLocation()) {
+ case "tab":
+ {
+ let normalWindow = await getNormalWindowReady(
+ context,
+ properties.windowId
+ );
+ let active = properties.active ?? true;
+ let tabmail = normalWindow.document.getElementById("tabmail");
+ let currentTab = tabmail.selectedTab;
+ let nativeTabInfo = tabmail.openTab("mailMessageTab", {
+ messageURI,
+ background: !active,
+ });
+ await new Promise(resolve =>
+ nativeTabInfo.chromeBrowser.addEventListener(
+ "MsgLoaded",
+ resolve,
+ { once: true }
+ )
+ );
+ tab = tabManager.convert(nativeTabInfo, currentTab);
+ }
+ break;
+
+ case "window":
+ {
+ // Handle window location.
+ let topNormalWindow = await getNormalWindowReady();
+ let messageWindow = topNormalWindow.MsgOpenNewWindowForMessage(
+ Services.io.newURI(messageURI)
+ );
+ await new Promise(resolve =>
+ messageWindow.addEventListener("MsgLoaded", resolve, {
+ once: true,
+ })
+ );
+ tab = tabManager.convert(messageWindow);
+ }
+ break;
+ }
+ return tab;
+ },
+ },
+ };
+ }
+};