diff options
Diffstat (limited to 'comm/mail/components/preferences/compose.js')
-rw-r--r-- | comm/mail/components/preferences/compose.js | 776 |
1 files changed, 776 insertions, 0 deletions
diff --git a/comm/mail/components/preferences/compose.js b/comm/mail/components/preferences/compose.js new file mode 100644 index 0000000000..7d9acf0622 --- /dev/null +++ b/comm/mail/components/preferences/compose.js @@ -0,0 +1,776 @@ +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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/. */ + +"use strict"; + +/* import-globals-from preferences.js */ + +var { InlineSpellChecker } = ChromeUtils.importESModule( + "resource://gre/modules/InlineSpellChecker.sys.mjs" +); + +// CloudFile account tools used by gCloudFile. +var { cloudFileAccounts } = ChromeUtils.import( + "resource:///modules/cloudFileAccounts.jsm" +); +var { E10SUtils } = ChromeUtils.importESModule( + "resource://gre/modules/E10SUtils.sys.mjs" +); +var { ExtensionParent } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionParent.sys.mjs" +); + +Preferences.addAll([ + { id: "mail.forward_message_mode", type: "int" }, + { id: "mail.forward_add_extension", type: "bool" }, + { id: "mail.SpellCheckBeforeSend", type: "bool" }, + { id: "mail.spellcheck.inline", type: "bool" }, + { id: "mail.warn_on_send_accel_key", type: "bool" }, + { id: "mail.compose.autosave", type: "bool" }, + { id: "mail.compose.autosaveinterval", type: "int" }, + { id: "mail.enable_autocomplete", type: "bool" }, + { id: "ldap_2.autoComplete.useDirectory", type: "bool" }, + { id: "ldap_2.autoComplete.directoryServer", type: "string" }, + { id: "pref.ldap.disable_button.edit_directories", type: "bool" }, + { id: "mail.collect_email_address_outgoing", type: "bool" }, + { id: "mail.collect_addressbook", type: "string" }, + { id: "spellchecker.dictionary", type: "unichar" }, + { id: "msgcompose.default_colors", type: "bool" }, + { id: "msgcompose.font_face", type: "string" }, + { id: "msgcompose.font_size", type: "string" }, + { id: "msgcompose.text_color", type: "string" }, + { id: "msgcompose.background_color", type: "string" }, + { id: "mail.compose.attachment_reminder", type: "bool" }, + { id: "mail.compose.default_to_paragraph", type: "bool" }, + { id: "mail.compose.big_attachments.notify", type: "bool" }, + { id: "mail.compose.big_attachments.threshold_kb", type: "int" }, + { id: "mail.default_send_format", type: "int" }, + { id: "mail.compose.add_link_preview", type: "bool" }, +]); + +var gComposePane = { + mSpellChecker: null, + + init() { + this.enableAutocomplete(); + + this.initLanguages(); + + this.populateFonts(); + + this.updateAutosave(); + + this.updateUseReaderDefaults(); + + this.updateAttachmentCheck(); + + this.updateEmailCollection(); + + this.initAbDefaultStartupDir(); + + this.setButtonColors(); + + // If BigFiles is disabled, hide the "Outgoing" tab, and the tab + // selectors, and bail out. + if (!Services.prefs.getBoolPref("mail.cloud_files.enabled")) { + // Hide the tab selector + let cloudFileBox = document.getElementById("cloudFileBox"); + cloudFileBox.hidden = true; + return; + } + + gCloudFile.init(); + }, + + attachmentReminderOptionsDialog() { + gSubDialog.open( + "chrome://messenger/content/preferences/attachmentReminder.xhtml", + { features: "resizable=no" } + ); + }, + + updateAutosave() { + gComposePane.enableElement( + document.getElementById("autoSaveInterval"), + Preferences.get("mail.compose.autosave").value + ); + }, + + updateUseReaderDefaults() { + let useReaderDefaultsChecked = Preferences.get( + "msgcompose.default_colors" + ).value; + gComposePane.enableElement( + document.getElementById("textColorLabel"), + !useReaderDefaultsChecked + ); + gComposePane.enableElement( + document.getElementById("backgroundColorLabel"), + !useReaderDefaultsChecked + ); + gComposePane.enableElement( + document.getElementById("textColorButton"), + !useReaderDefaultsChecked + ); + gComposePane.enableElement( + document.getElementById("backgroundColorButton"), + !useReaderDefaultsChecked + ); + }, + + updateAttachmentCheck() { + gComposePane.enableElement( + document.getElementById("attachment_reminder_button"), + Preferences.get("mail.compose.attachment_reminder").value + ); + }, + + updateEmailCollection() { + gComposePane.enableElement( + document.getElementById("localDirectoriesList"), + Preferences.get("mail.collect_email_address_outgoing").value + ); + }, + + enableElement(aElement, aEnable) { + let pref = aElement.getAttribute("preference"); + let prefIsLocked = pref ? Preferences.get(pref).locked : false; + aElement.disabled = !aEnable || prefIsLocked; + }, + + enableAutocomplete() { + let acLDAPPref = Preferences.get("ldap_2.autoComplete.useDirectory").value; + gComposePane.enableElement( + document.getElementById("directoriesList"), + acLDAPPref + ); + gComposePane.enableElement( + document.getElementById("editButton"), + acLDAPPref + ); + }, + + editDirectories() { + gSubDialog.open( + "chrome://messenger/content/addressbook/pref-editdirectories.xhtml" + ); + }, + + initAbDefaultStartupDir() { + if (!this.startupDirListener.inited) { + this.startupDirListener.load(); + } + + let dirList = document.getElementById("defaultStartupDirList"); + if (Services.prefs.getBoolPref("mail.addr_book.view.startupURIisDefault")) { + // Some directory is the default. + let startupURI = Services.prefs.getCharPref( + "mail.addr_book.view.startupURI" + ); + dirList.value = startupURI; + } else { + // Choose item meaning there is no default startup directory any more. + dirList.value = ""; + } + }, + + setButtonColors() { + document.getElementById("textColorButton").value = Preferences.get( + "msgcompose.text_color" + ).value; + document.getElementById("backgroundColorButton").value = Preferences.get( + "msgcompose.background_color" + ).value; + }, + + setDefaultStartupDir(aDirURI) { + if (aDirURI) { + // Some AB directory was selected. Set prefs to make this directory + // the default view when starting up the main AB. + Services.prefs.setCharPref("mail.addr_book.view.startupURI", aDirURI); + Services.prefs.setBoolPref( + "mail.addr_book.view.startupURIisDefault", + true + ); + } else { + // Set pref that there's no default startup view directory any more. + Services.prefs.setBoolPref( + "mail.addr_book.view.startupURIisDefault", + false + ); + } + }, + + async initLanguages() { + let languageList = document.getElementById("dictionaryList"); + this.mSpellChecker = Cc["@mozilla.org/spellchecker/engine;1"].getService( + Ci.mozISpellCheckingEngine + ); + + // Get the list of dictionaries from the spellchecker. + + let dictList = this.mSpellChecker.getDictionaryList(); + + // HACK: calling sortDictionaryList may fail the first time due to + // synchronous loading of the .ftl files. If we load the files and wait + // for a known value asynchronously, no such failure will happen. + await new Localization([ + "toolkit/intl/languageNames.ftl", + "toolkit/intl/regionNames.ftl", + ]).formatValue("language-name-en"); + let sortedList = new InlineSpellChecker().sortDictionaryList(dictList); + let activeDictionaries = Services.prefs + .getCharPref("spellchecker.dictionary") + .split(","); + let template = document.getElementById("dictionaryListItem"); + languageList.replaceChildren( + ...sortedList.map(({ displayName, localeCode }) => { + let item = template.content.cloneNode(true).firstElementChild; + item.querySelector(".checkbox-label").textContent = displayName; + let input = item.querySelector("input"); + input.setAttribute("value", localeCode); + input.addEventListener("change", event => { + let language = event.target.value; + let dicts = Services.prefs + .getCharPref("spellchecker.dictionary") + .split(",") + .filter(Boolean); + if (!event.target.checked) { + dicts = dicts.filter(item => item != language); + } else { + dicts.push(language); + } + Services.prefs.setCharPref( + "spellchecker.dictionary", + dicts.join(",") + ); + }); + input.checked = activeDictionaries.includes(localeCode); + return item; + }) + ); + }, + + populateFonts() { + var fontsList = document.getElementById("FontSelect"); + try { + var enumerator = Cc["@mozilla.org/gfx/fontenumerator;1"].getService( + Ci.nsIFontEnumerator + ); + var localFonts = enumerator.EnumerateAllFonts(); + for (let i = 0; i < localFonts.length; ++i) { + // Remove Linux system generic fonts that collide with CSS generic fonts. + if ( + localFonts[i] != "" && + localFonts[i] != "serif" && + localFonts[i] != "sans-serif" && + localFonts[i] != "monospace" + ) { + fontsList.appendItem(localFonts[i], localFonts[i]); + } + } + } catch (e) {} + // Choose the item after the list is completely generated. + var preference = Preferences.get(fontsList.getAttribute("preference")); + fontsList.value = preference.value; + }, + + restoreHTMLDefaults() { + // reset throws an exception if the pref value is already the default so + // work around that with some try/catch exception handling + try { + Preferences.get("msgcompose.font_face").reset(); + } catch (ex) {} + + try { + Preferences.get("msgcompose.font_size").reset(); + } catch (ex) {} + + try { + Preferences.get("msgcompose.text_color").reset(); + } catch (ex) {} + + try { + Preferences.get("msgcompose.background_color").reset(); + } catch (ex) {} + + try { + Preferences.get("msgcompose.default_colors").reset(); + } catch (ex) {} + + this.updateUseReaderDefaults(); + this.setButtonColors(); + }, + + startupDirListener: { + inited: false, + domain: "mail.addr_book.view.startupURI", + observe(subject, topic, prefName) { + if (topic != "nsPref:changed") { + return; + } + + // If the default startup directory prefs have changed, + // reinitialize the default startup dir picker to show the new value. + gComposePane.initAbDefaultStartupDir(); + }, + load() { + // Observe changes of our prefs. + Services.prefs.addObserver(this.domain, this); + // Unload the pref observer when preferences window is closed. + window.addEventListener("unload", () => this.unload(), true); + this.inited = true; + }, + + unload(event) { + Services.prefs.removeObserver( + gComposePane.startupDirListener.domain, + gComposePane.startupDirListener + ); + }, + }, +}; + +var gCloudFile = { + _initialized: false, + _list: null, + _buttonContainer: null, + _listContainer: null, + _settings: null, + _tabpanel: null, + _settingsPanelWrap: null, + _defaultPanel: null, + + get _strings() { + return Services.strings.createBundle( + "chrome://messenger/locale/preferences/applications.properties" + ); + }, + + init() { + this._list = document.getElementById("cloudFileView"); + this._buttonContainer = document.getElementById( + "addCloudFileAccountButtons" + ); + this._addAccountButton = document.getElementById("addCloudFileAccount"); + this._listContainer = document.getElementById( + "addCloudFileAccountListItems" + ); + this._removeAccountButton = document.getElementById( + "removeCloudFileAccount" + ); + this._defaultPanel = document.getElementById("cloudFileDefaultPanel"); + this._settingsPanelWrap = document.getElementById( + "cloudFileSettingsWrapper" + ); + + this.updateThreshold(); + this.rebuildView(); + + window.addEventListener("unload", this, { capture: false, once: true }); + + this._onAccountConfigured = this._onAccountConfigured.bind(this); + this._onProviderRegistered = this._onProviderRegistered.bind(this); + this._onProviderUnregistered = this._onProviderUnregistered.bind(this); + cloudFileAccounts.on("accountConfigured", this._onAccountConfigured); + cloudFileAccounts.on("providerRegistered", this._onProviderRegistered); + cloudFileAccounts.on("providerUnregistered", this._onProviderUnregistered); + + let element = document.getElementById("cloudFileThreshold"); + Preferences.addSyncFromPrefListener(element, () => this.readThreshold()); + Preferences.addSyncToPrefListener(element, () => this.writeThreshold()); + + this._initialized = true; + }, + + destroy() { + // Remove any controllers or observers here. + cloudFileAccounts.off("accountConfigured", this._onAccountConfigured); + cloudFileAccounts.off("providerRegistered", this._onProviderRegistered); + cloudFileAccounts.off("providerUnregistered", this._onProviderUnregistered); + }, + + _onAccountConfigured(event, account) { + for (let item of this._list.children) { + if (item.value == account.accountKey) { + item.querySelector(".configuredWarning").hidden = account.configured; + } + } + }, + + _onProviderRegistered(event, provider) { + let accounts = cloudFileAccounts.getAccountsForType(provider.type); + accounts.sort(this._sortDisplayNames); + + // Always add newly-enabled accounts to the end of the list, this makes + // it clearer to users what's happening. + for (let account of accounts) { + let item = this.makeRichListItemForAccount(account); + this._list.appendChild(item); + } + + this._buttonContainer.appendChild(this.makeButtonForProvider(provider)); + this._listContainer.appendChild(this.makeListItemForProvider(provider)); + }, + + _onProviderUnregistered(event, type) { + for (let item of [...this._list.children]) { + // If the provider is unregistered, getAccount returns null. + if (!cloudFileAccounts.getAccount(item.value)) { + if (item.hasAttribute("selected")) { + this._defaultPanel.hidden = false; + this._settingsPanelWrap.hidden = true; + if (this._settings) { + this._settings.remove(); + } + this._removeAccountButton.disabled = true; + } + item.remove(); + } + } + + for (let button of this._buttonContainer.children) { + if (button.getAttribute("value") == type) { + button.remove(); + } + } + + for (let item of this._listContainer.children) { + if (item.getAttribute("value") == type) { + item.remove(); + } + } + + if (this._buttonContainer.childElementCount < 1) { + this._buttonContainer.hidden = false; + this._addAccountButton.hidden = true; + } + }, + + makeRichListItemForAccount(aAccount) { + let rli = document.createXULElement("richlistitem"); + rli.setAttribute("align", "center"); + rli.classList.add("cloudfileAccount", "input-container"); + rli.setAttribute("value", aAccount.accountKey); + + let icon = document.createElement("img"); + icon.classList.add("typeIcon"); + if (aAccount.iconURL) { + icon.setAttribute("src", aAccount.iconURL); + } + icon.setAttribute("alt", ""); + rli.appendChild(icon); + + let label = document.createXULElement("label"); + label.setAttribute("crop", "end"); + label.setAttribute("flex", "1"); + label.setAttribute( + "value", + cloudFileAccounts.getDisplayName(aAccount.accountKey) + ); + label.addEventListener("click", this, true); + rli.appendChild(label); + + let input = document.createElement("input"); + input.setAttribute("type", "text"); + input.setAttribute("hidden", "hidden"); + input.addEventListener("blur", this); + input.addEventListener("keypress", this); + rli.appendChild(input); + + let warningIcon = document.createElement("img"); + warningIcon.setAttribute("class", "configuredWarning typeIcon"); + warningIcon.setAttribute("src", "chrome://global/skin/icons/warning.svg"); + // "title" provides the accessible name, not "alt". + warningIcon.setAttribute( + "title", + this._strings.GetStringFromName("notConfiguredYet") + ); + if (aAccount.configured) { + warningIcon.hidden = true; + } + rli.appendChild(warningIcon); + + return rli; + }, + + makeButtonForProvider(provider) { + let button = document.createXULElement("button"); + button.setAttribute("value", provider.type); + button.setAttribute( + "label", + this._strings.formatStringFromName("addProvider", [provider.displayName]) + ); + button.setAttribute( + "oncommand", + `gCloudFile.addCloudFileAccount("${provider.type}")` + ); + button.style.listStyleImage = `url("${provider.iconURL}")`; + return button; + }, + + makeListItemForProvider(provider) { + let menuitem = document.createXULElement("menuitem"); + menuitem.classList.add("menuitem-iconic"); + menuitem.setAttribute("value", provider.type); + menuitem.setAttribute("label", provider.displayName); + menuitem.setAttribute("image", provider.iconURL); + return menuitem; + }, + + // Sort the accounts by displayName. + _sortDisplayNames(a, b) { + let aName = a.displayName.toLowerCase(); + let bName = b.displayName.toLowerCase(); + return aName.localeCompare(bName); + }, + + rebuildView() { + // Clear the list of entries. + while (this._list.hasChildNodes()) { + this._list.lastChild.remove(); + } + + let accounts = cloudFileAccounts.accounts; + accounts.sort(this._sortDisplayNames); + + for (let account of accounts) { + let rli = this.makeRichListItemForAccount(account); + this._list.appendChild(rli); + } + + while (this._buttonContainer.hasChildNodes()) { + this._buttonContainer.lastChild.remove(); + } + + let providers = cloudFileAccounts.providers; + providers.sort(this._sortDisplayNames); + for (let provider of providers) { + this._buttonContainer.appendChild(this.makeButtonForProvider(provider)); + this._listContainer.appendChild(this.makeListItemForProvider(provider)); + } + }, + + onSelectionChanged(aEvent) { + if (!this._initialized || aEvent.target != this._list) { + return; + } + + // Get the selected item + let selection = this._list.selectedItem; + this._removeAccountButton.disabled = !selection; + if (!selection) { + this._defaultPanel.hidden = false; + this._settingsPanelWrap.hidden = true; + if (this._settings) { + this._settings.remove(); + } + return; + } + + this._showAccountInfo(selection.value); + }, + + _showAccountInfo(aAccountKey) { + let account = cloudFileAccounts.getAccount(aAccountKey); + this._defaultPanel.hidden = true; + this._settingsPanelWrap.hidden = false; + + let url = account.managementURL + `?accountId=${account.accountKey}`; + + let browser = document.createXULElement("browser"); + browser.setAttribute("type", "content"); + browser.setAttribute("remote", "true"); + browser.setAttribute("remoteType", E10SUtils.EXTENSION_REMOTE_TYPE); + browser.setAttribute("forcemessagemanager", "true"); + if (account.extension) { + browser.setAttribute( + "initialBrowsingContextGroupId", + account.extension.policy.browsingContextGroupId + ); + } + browser.setAttribute("disableglobalhistory", "true"); + browser.setAttribute("messagemanagergroup", "webext-browsers"); + browser.setAttribute("autocompletepopup", "PopupAutoComplete"); + browser.setAttribute("selectmenulist", "ContentSelectDropdown"); + + browser.setAttribute("flex", "1"); + // Allows keeping dialog background color without hoops. + browser.setAttribute("transparent", "true"); + + // If we have a past browser, we replace it. Else append to the wrapper. + if (this._settings) { + this._settings.remove(); + } + + this._settingsPanelWrap.appendChild(browser); + this._settings = browser; + + ExtensionParent.apiManager.emit("extension-browser-inserted", browser); + browser.messageManager.loadFrameScript( + "chrome://extensions/content/ext-browser-content.js", + false, + true + ); + + let options = account.browserStyle + ? { stylesheets: ExtensionParent.extensionStylesheets } + : {}; + browser.messageManager.sendAsyncMessage("Extension:InitBrowser", options); + + browser.fixupAndLoadURIString(url, { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + }, + + onListOverflow() { + if (this._buttonContainer.childElementCount > 1) { + this._buttonContainer.hidden = true; + this._addAccountButton.hidden = false; + } + }, + + addCloudFileAccount(aType) { + let account = cloudFileAccounts.createAccount(aType); + if (!account) { + return; + } + + let rli = this.makeRichListItemForAccount(account); + this._list.appendChild(rli); + this._list.selectItem(rli); + this._addAccountButton.removeAttribute("image"); + this._addAccountButton.setAttribute( + "label", + this._addAccountButton.getAttribute("defaultlabel") + ); + this._removeAccountButton.disabled = false; + }, + + removeCloudFileAccount() { + // Get the selected account key + let selection = this._list.selectedItem; + if (!selection) { + return; + } + + let accountKey = selection.value; + let accountName = cloudFileAccounts.getDisplayName(accountKey); + // Does the user really want to remove this account? + let confirmMessage = this._strings.formatStringFromName( + "dialog_removeAccount", + [accountName] + ); + + if (Services.prompt.confirm(null, "", confirmMessage)) { + this._list.clearSelection(); + cloudFileAccounts.removeAccount(accountKey); + let rli = this._list.querySelector( + "richlistitem[value='" + accountKey + "']" + ); + rli.remove(); + this._defaultPanel.hidden = false; + this._settingsPanelWrap.hidden = true; + if (this._settings) { + this._settings.remove(); + } + } + }, + + handleEvent(aEvent) { + switch (aEvent.type) { + case "unload": + this.destroy(); + break; + case "click": { + let label = aEvent.target; + let item = label.parentNode; + let input = item.querySelector("input"); + if (!item.selected) { + return; + } + label.hidden = true; + input.value = label.value; + input.removeAttribute("hidden"); + input.focus(); + break; + } + case "blur": { + let input = aEvent.target; + let item = input.parentNode; + let label = item.querySelector("label"); + cloudFileAccounts.setDisplayName(item.value, input.value); + label.value = input.value; + label.hidden = false; + input.setAttribute("hidden", "hidden"); + break; + } + case "keypress": { + let input = aEvent.target; + let item = input.parentNode; + let label = item.querySelector("label"); + + if (aEvent.key == "Enter") { + cloudFileAccounts.setDisplayName(item.value, input.value); + label.value = input.value; + label.hidden = false; + input.setAttribute("hidden", "hidden"); + gCloudFile._list.focus(); + + aEvent.preventDefault(); + } else if (aEvent.key == "Escape") { + input.value = label.value; + label.hidden = false; + input.setAttribute("hidden", "hidden"); + gCloudFile._list.focus(); + + aEvent.preventDefault(); + } + } + } + }, + + readThreshold() { + let pref = Preferences.get("mail.compose.big_attachments.threshold_kb"); + return pref.value / 1024; + }, + + writeThreshold() { + let threshold = document.getElementById("cloudFileThreshold"); + let intValue = parseInt(threshold.value, 10); + return isNaN(intValue) ? 0 : intValue * 1024; + }, + + updateThreshold() { + document.getElementById("cloudFileThreshold").disabled = !Preferences.get( + "mail.compose.big_attachments.notify" + ).value; + }, +}; + +Preferences.get("mail.compose.autosave").on( + "change", + gComposePane.updateAutosave +); +Preferences.get("mail.compose.attachment_reminder").on( + "change", + gComposePane.updateAttachmentCheck +); +Preferences.get("msgcompose.default_colors").on( + "change", + gComposePane.updateUseReaderDefaults +); +Preferences.get("ldap_2.autoComplete.useDirectory").on( + "change", + gComposePane.enableAutocomplete +); +Preferences.get("mail.collect_email_address_outgoing").on( + "change", + gComposePane.updateEmailCollection +); +Preferences.get("mail.compose.big_attachments.notify").on( + "change", + gCloudFile.updateThreshold +); |