diff options
Diffstat (limited to 'browser/components/newtab/lib/AboutPreferences.jsm')
-rw-r--r-- | browser/components/newtab/lib/AboutPreferences.jsm | 311 |
1 files changed, 311 insertions, 0 deletions
diff --git a/browser/components/newtab/lib/AboutPreferences.jsm b/browser/components/newtab/lib/AboutPreferences.jsm new file mode 100644 index 0000000000..4906728eb2 --- /dev/null +++ b/browser/components/newtab/lib/AboutPreferences.jsm @@ -0,0 +1,311 @@ +/* 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"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { actionTypes: at, actionCreators: ac } = ChromeUtils.importESModule( + "resource://activity-stream/common/Actions.sys.mjs" +); + +const HTML_NS = "http://www.w3.org/1999/xhtml"; +const PREFERENCES_LOADED_EVENT = "home-pane-loaded"; + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + NimbusFeatures: "resource://nimbus/ExperimentAPI.jsm", +}); + +// These "section" objects are formatted in a way to be similar to the ones from +// SectionsManager to construct the preferences view. +const PREFS_BEFORE_SECTIONS = () => [ + { + id: "search", + pref: { + feed: "showSearch", + titleString: "home-prefs-search-header", + }, + icon: "chrome://global/skin/icons/search-glass.svg", + }, + { + id: "topsites", + pref: { + feed: "feeds.topsites", + titleString: "home-prefs-shortcuts-header", + descString: "home-prefs-shortcuts-description", + get nestedPrefs() { + return Services.prefs.getBoolPref("browser.topsites.useRemoteSetting") + ? [ + { + name: "showSponsoredTopSites", + titleString: "home-prefs-shortcuts-by-option-sponsored", + eventSource: "SPONSORED_TOP_SITES", + }, + ] + : []; + }, + }, + icon: "chrome://browser/skin/topsites.svg", + maxRows: 4, + rowsPref: "topSitesRows", + eventSource: "TOP_SITES", + }, +]; + +const PREFS_AFTER_SECTIONS = () => [ + { + id: "snippets", + pref: { + feed: "feeds.snippets", + titleString: "home-prefs-snippets-header", + descString: "home-prefs-snippets-description-new", + }, + icon: "chrome://global/skin/icons/info.svg", + eventSource: "SNIPPETS", + }, +]; + +class AboutPreferences { + init() { + Services.obs.addObserver(this, PREFERENCES_LOADED_EVENT); + } + + uninit() { + Services.obs.removeObserver(this, PREFERENCES_LOADED_EVENT); + } + + onAction(action) { + switch (action.type) { + case at.INIT: + this.init(); + break; + case at.UNINIT: + this.uninit(); + break; + case at.SETTINGS_OPEN: + action._target.browser.ownerGlobal.openPreferences("paneHome"); + break; + // This is used to open the web extension settings page for an extension + case at.OPEN_WEBEXT_SETTINGS: + action._target.browser.ownerGlobal.BrowserOpenAddonsMgr( + `addons://detail/${encodeURIComponent(action.data)}` + ); + break; + } + } + + handleDiscoverySettings(sections) { + // Deep copy object to not modify original Sections state in store + let sectionsCopy = JSON.parse(JSON.stringify(sections)); + sectionsCopy.forEach(obj => { + if (obj.id === "topstories") { + obj.rowsPref = ""; + } + }); + return sectionsCopy; + } + + setupUserEvent(element, eventSource) { + element.addEventListener("command", e => { + const { checked } = e.target; + if (typeof checked === "boolean") { + this.store.dispatch( + ac.UserEvent({ + event: "PREF_CHANGED", + source: eventSource, + value: { status: checked, menu_source: "ABOUT_PREFERENCES" }, + }) + ); + } + }); + } + + observe(window) { + const discoveryStreamConfig = this.store.getState().DiscoveryStream.config; + let sections = this.store.getState().Sections; + + if (discoveryStreamConfig.enabled) { + sections = this.handleDiscoverySettings(sections); + } + + const featureConfig = lazy.NimbusFeatures.newtab.getAllVariables() || {}; + + this.renderPreferences(window, [ + ...PREFS_BEFORE_SECTIONS(featureConfig), + ...sections, + ...PREFS_AFTER_SECTIONS(featureConfig), + ]); + } + + /** + * Render preferences to an about:preferences content window with the provided + * preferences structure. + */ + renderPreferences({ document, Preferences, gHomePane }, prefStructure) { + // Helper to create a new element and append it + const createAppend = (tag, parent, options) => + parent.appendChild(document.createXULElement(tag, options)); + + // Helper to get fluentIDs sometimes encase in an object + const getString = message => + typeof message !== "object" ? message : message.id; + + // Helper to link a UI element to a preference for updating + const linkPref = (element, name, type) => { + const fullPref = `browser.newtabpage.activity-stream.${name}`; + element.setAttribute("preference", fullPref); + Preferences.add({ id: fullPref, type }); + + // Prevent changing the UI if the preference can't be changed + element.disabled = Preferences.get(fullPref).locked; + }; + + // Insert a new group immediately after the homepage one + const homeGroup = document.getElementById("homepageGroup"); + const contentsGroup = homeGroup.insertAdjacentElement( + "afterend", + homeGroup.cloneNode() + ); + contentsGroup.id = "homeContentsGroup"; + contentsGroup.setAttribute("data-subcategory", "contents"); + const homeHeader = createAppend("label", contentsGroup).appendChild( + document.createElementNS(HTML_NS, "h2") + ); + document.l10n.setAttributes(homeHeader, "home-prefs-content-header2"); + + const homeDescription = createAppend("description", contentsGroup); + document.l10n.setAttributes( + homeDescription, + "home-prefs-content-description2" + ); + + // Add preferences for each section + prefStructure.forEach(sectionData => { + const { + id, + pref: prefData, + icon = "webextension", + maxRows, + rowsPref, + shouldHidePref, + eventSource, + } = sectionData; + const { feed: name, titleString = {}, descString, nestedPrefs = [] } = + prefData || {}; + + // Don't show any sections that we don't want to expose in preferences UI + if (shouldHidePref) { + return; + } + + // Use full icon spec for certain protocols or fall back to packaged icon + const iconUrl = !icon.search(/^(chrome|moz-extension|resource):/) + ? icon + : `chrome://activity-stream/content/data/content/assets/glyph-${icon}-16.svg`; + + // Add the main preference for turning on/off a section + const sectionVbox = createAppend("vbox", contentsGroup); + sectionVbox.setAttribute("data-subcategory", id); + const checkbox = createAppend("checkbox", sectionVbox); + checkbox.classList.add("section-checkbox"); + checkbox.setAttribute("src", iconUrl); + // Setup a user event if we have an event source for this pref. + if (eventSource) { + this.setupUserEvent(checkbox, eventSource); + } + document.l10n.setAttributes( + checkbox, + getString(titleString), + titleString.values + ); + + linkPref(checkbox, name, "bool"); + + // Specially add a link for stories + if (id === "topstories") { + const sponsoredHbox = createAppend("hbox", sectionVbox); + sponsoredHbox.setAttribute("align", "center"); + sponsoredHbox.appendChild(checkbox); + checkbox.classList.add("tail-with-learn-more"); + + const link = createAppend("label", sponsoredHbox, { is: "text-link" }); + link.classList.add("learn-sponsored"); + link.setAttribute("href", sectionData.pref.learnMore.link.href); + document.l10n.setAttributes(link, sectionData.pref.learnMore.link.id); + } + + // Add more details for the section (e.g., description, more prefs) + const detailVbox = createAppend("vbox", sectionVbox); + detailVbox.classList.add("indent"); + if (descString) { + const label = createAppend("label", detailVbox); + label.classList.add("indent"); + document.l10n.setAttributes( + label, + getString(descString), + descString.values + ); + + // Add a rows dropdown if we have a pref to control and a maximum + if (rowsPref && maxRows) { + const detailHbox = createAppend("hbox", detailVbox); + detailHbox.setAttribute("align", "center"); + label.setAttribute("flex", 1); + detailHbox.appendChild(label); + + // Add box so the search tooltip is positioned correctly + const tooltipBox = createAppend("hbox", detailHbox); + + // Add appropriate number of localized entries to the dropdown + const menulist = createAppend("menulist", tooltipBox); + menulist.setAttribute("crop", "none"); + const menupopup = createAppend("menupopup", menulist); + for (let num = 1; num <= maxRows; num++) { + const item = createAppend("menuitem", menupopup); + document.l10n.setAttributes( + item, + "home-prefs-sections-rows-option", + { num } + ); + item.setAttribute("value", num); + } + linkPref(menulist, rowsPref, "int"); + } + } + + const subChecks = []; + const fullName = `browser.newtabpage.activity-stream.${sectionData.pref.feed}`; + const pref = Preferences.get(fullName); + + // Add a checkbox pref for any nested preferences + nestedPrefs.forEach(nested => { + const subcheck = createAppend("checkbox", detailVbox); + // Setup a user event if we have an event source for this pref. + if (nested.eventSource) { + this.setupUserEvent(subcheck, nested.eventSource); + } + subcheck.classList.add("indent"); + document.l10n.setAttributes(subcheck, nested.titleString); + linkPref(subcheck, nested.name, "bool"); + subChecks.push(subcheck); + subcheck.disabled = !pref._value; + subcheck.hidden = nested.hidden; + }); + + // Disable any nested checkboxes if the parent pref is not enabled. + pref.on("change", () => { + subChecks.forEach(subcheck => { + subcheck.disabled = !pref._value; + }); + }); + }); + + // Update the visibility of the Restore Defaults btn based on checked prefs + gHomePane.toggleRestoreDefaultsBtn(); + } +} + +const EXPORTED_SYMBOLS = ["AboutPreferences", "PREFERENCES_LOADED_EVENT"]; |