tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

AboutPreferences.sys.mjs (11877B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import {
      6  actionTypes as at,
      7  actionCreators as ac,
      8 } from "resource://newtab/common/Actions.mjs";
      9 
     10 export const PREFERENCES_LOADED_EVENT = "home-pane-loaded";
     11 
     12 // These "section" objects are formatted in a way to be similar to the ones from
     13 // SectionsManager to construct the preferences view.
     14 const PREFS_FOR_SETTINGS = () => [
     15  {
     16    id: "web-search",
     17    pref: {
     18      feed: "showSearch",
     19      titleString: "home-prefs-search-header",
     20    },
     21  },
     22  {
     23    id: "weather",
     24    pref: {
     25      feed: "showWeather",
     26      titleString: "home-prefs-weather-header",
     27      descString: "home-prefs-weather-description",
     28      learnMore: {
     29        link: {
     30          href: "https://support.mozilla.org/kb/customize-items-on-firefox-new-tab-page",
     31          id: "home-prefs-weather-learn-more-link",
     32        },
     33      },
     34    },
     35    eventSource: "WEATHER",
     36    shouldHidePref: !Services.prefs.getBoolPref(
     37      "browser.newtabpage.activity-stream.system.showWeather",
     38      false
     39    ),
     40  },
     41  {
     42    id: "topsites",
     43    pref: {
     44      feed: "feeds.topsites",
     45      titleString: "home-prefs-shortcuts-header",
     46      descString: "home-prefs-shortcuts-description",
     47    },
     48    maxRows: 4,
     49    rowsPref: "topSitesRows",
     50    eventSource: "TOP_SITES",
     51  },
     52  {
     53    id: "topstories",
     54    pref: {
     55      feed: "feeds.section.topstories",
     56      titleString: {
     57        id: "home-prefs-recommended-by-header-generic",
     58      },
     59      descString: {
     60        id: "home-prefs-recommended-by-description-generic",
     61      },
     62    },
     63    shouldHidePref: !Services.prefs.getBoolPref(
     64      "browser.newtabpage.activity-stream.feeds.system.topstories",
     65      true
     66    ),
     67    eventSource: "TOP_STORIES",
     68  },
     69  {
     70    id: "support-firefox",
     71    pref: {
     72      feed: "showSponsoredCheckboxes",
     73      titleString: "home-prefs-support-firefox-header",
     74      nestedPrefs: [
     75        {
     76          name: "showSponsoredTopSites",
     77          titleString: "home-prefs-shortcuts-by-option-sponsored",
     78          eventSource: "SPONSORED_TOP_SITES",
     79        },
     80        {
     81          name: "showSponsored",
     82          titleString: "home-prefs-recommended-by-option-sponsored-stories",
     83          eventSource: "POCKET_SPOCS",
     84          shouldHidePref: !Services.prefs.getBoolPref(
     85            "browser.newtabpage.activity-stream.feeds.system.topstories",
     86            true
     87          ),
     88          shouldDisablePref: !Services.prefs.getBoolPref(
     89            "browser.newtabpage.activity-stream.feeds.section.topstories",
     90            true
     91          ),
     92        },
     93      ],
     94    },
     95  },
     96 ];
     97 
     98 export class AboutPreferences {
     99  init() {
    100    Services.obs.addObserver(this, PREFERENCES_LOADED_EVENT);
    101  }
    102 
    103  uninit() {
    104    Services.obs.removeObserver(this, PREFERENCES_LOADED_EVENT);
    105  }
    106 
    107  onAction(action) {
    108    switch (action.type) {
    109      case at.INIT:
    110        this.init();
    111        break;
    112      case at.UNINIT:
    113        this.uninit();
    114        break;
    115      case at.SETTINGS_OPEN:
    116        action._target.browser.ownerGlobal.openPreferences("paneHome");
    117        break;
    118      // This is used to open the web extension settings page for an extension
    119      case at.OPEN_WEBEXT_SETTINGS:
    120        action._target.browser.ownerGlobal.BrowserAddonUI.openAddonsMgr(
    121          `addons://detail/${encodeURIComponent(action.data)}`
    122        );
    123        break;
    124    }
    125  }
    126 
    127  setupUserEvent(element, eventSource) {
    128    element.addEventListener("command", e => {
    129      const { checked } = e.target;
    130      if (typeof checked === "boolean") {
    131        this.store.dispatch(
    132          ac.UserEvent({
    133            event: "PREF_CHANGED",
    134            source: eventSource,
    135            value: { status: checked, menu_source: "ABOUT_PREFERENCES" },
    136          })
    137        );
    138      }
    139    });
    140  }
    141 
    142  observe(window) {
    143    const { document, Preferences } = window;
    144 
    145    // Extract just the "Recent activity" pref info from SectionsManager as we have everything else already
    146    const highlights = this.store
    147      .getState()
    148      .Sections.find(el => el.id === "highlights");
    149 
    150    const allSections = [...PREFS_FOR_SETTINGS(), highlights];
    151 
    152    // Render the preferences
    153    allSections.forEach(pref => {
    154      this.renderPreferenceSection(pref, document, Preferences);
    155    });
    156 
    157    // Update the visibility of the Restore Defaults button based on checked prefs
    158    this.toggleRestoreDefaults(window.gHomePane);
    159  }
    160 
    161  /**
    162   * Render a single preference with all the details, e.g. description, links,
    163   * more granular preferences.
    164   *
    165   * @param sectionData
    166   * @param document
    167   * @param Preferences
    168   */
    169 
    170  /**
    171   * We can remove this eslint exception once the Settings redesign is complete.
    172   * In fact, we can probably remove this entire method.
    173   */
    174  // eslint-disable-next-line max-statements
    175  renderPreferenceSection(sectionData, document, Preferences) {
    176    /* Do not render old-style settings if new settings UI is enabled - this is needed to avoid
    177     * registering prefs twice and ensuing errors */
    178    if (Services.prefs.getBoolPref("browser.settings-redesign.enabled")) {
    179      return;
    180    }
    181 
    182    const {
    183      id,
    184      pref: prefData,
    185      maxRows,
    186      rowsPref,
    187      shouldHidePref,
    188      eventSource,
    189    } = sectionData;
    190    const {
    191      feed: name,
    192      titleString = {},
    193      descString,
    194      nestedPrefs = [],
    195    } = prefData || {};
    196 
    197    // Helper to link a UI element to a preference for updating
    198    const linkPref = (element, prefName, type) => {
    199      const fullPref = `browser.newtabpage.activity-stream.${prefName}`;
    200      element.setAttribute("preference", fullPref);
    201      Preferences.add({ id: fullPref, type });
    202 
    203      // Prevent changing the UI if the preference can't be changed
    204      element.disabled = Preferences.get(fullPref).locked;
    205    };
    206 
    207    // Don't show any sections that we don't want to expose in preferences UI
    208    if (shouldHidePref) {
    209      return;
    210    }
    211 
    212    // Add the main preference for turning on/off a section
    213    const sectionVbox = document.getElementById(id);
    214    sectionVbox.setAttribute("data-subcategory", id);
    215    const checkbox = this.createAppend(document, "checkbox", sectionVbox);
    216    checkbox.classList.add("section-checkbox");
    217    // Set up a user event if we have an event source for this pref.
    218    if (eventSource) {
    219      this.setupUserEvent(checkbox, eventSource);
    220    }
    221    document.l10n.setAttributes(
    222      checkbox,
    223      this.getString(titleString),
    224      titleString.values
    225    );
    226 
    227    linkPref(checkbox, name, "bool");
    228 
    229    // Specially add a link for Weather
    230    if (id === "weather") {
    231      const hboxWithLink = this.createAppend(document, "hbox", sectionVbox);
    232      hboxWithLink.appendChild(checkbox);
    233      checkbox.classList.add("tail-with-learn-more");
    234 
    235      const link = this.createAppend(document, "label", hboxWithLink, {
    236        is: "text-link",
    237      });
    238      link.setAttribute("href", sectionData.pref.learnMore.link.href);
    239      document.l10n.setAttributes(link, sectionData.pref.learnMore.link.id);
    240    }
    241 
    242    // Add more details for the section (e.g., description, more prefs)
    243    const detailVbox = this.createAppend(document, "vbox", sectionVbox);
    244    detailVbox.classList.add("indent");
    245    if (descString) {
    246      const description = this.createAppend(
    247        document,
    248        "description",
    249        detailVbox
    250      );
    251      description.classList.add("text-deemphasized");
    252      document.l10n.setAttributes(
    253        description,
    254        this.getString(descString),
    255        descString.values
    256      );
    257 
    258      // Add a rows dropdown if we have a pref to control and a maximum
    259      if (rowsPref && maxRows) {
    260        const detailHbox = this.createAppend(document, "hbox", detailVbox);
    261        detailHbox.setAttribute("align", "center");
    262        description.setAttribute("flex", 1);
    263        detailHbox.appendChild(description);
    264 
    265        // Add box so the search tooltip is positioned correctly
    266        const tooltipBox = this.createAppend(document, "hbox", detailHbox);
    267 
    268        // Add appropriate number of localized entries to the dropdown
    269        const menulist = this.createAppend(document, "menulist", tooltipBox);
    270        menulist.setAttribute("crop", "none");
    271        const menupopup = this.createAppend(document, "menupopup", menulist);
    272        for (let num = 1; num <= maxRows; num++) {
    273          const item = this.createAppend(document, "menuitem", menupopup);
    274          document.l10n.setAttributes(item, "home-prefs-sections-rows-option", {
    275            num,
    276          });
    277          item.setAttribute("value", num);
    278        }
    279        linkPref(menulist, rowsPref, "int");
    280      }
    281    }
    282 
    283    const subChecks = [];
    284    const fullName = `browser.newtabpage.activity-stream.${sectionData.pref.feed}`;
    285    const pref = Preferences.get(fullName);
    286 
    287    // Add a checkbox pref for any nested preferences
    288    nestedPrefs.forEach(nested => {
    289      if (nested.shouldHidePref !== true) {
    290        const subcheck = this.createAppend(document, "checkbox", detailVbox);
    291        // Set up a user event if we have an event source for this pref.
    292        if (nested.eventSource) {
    293          this.setupUserEvent(subcheck, nested.eventSource);
    294        }
    295        document.l10n.setAttributes(subcheck, nested.titleString);
    296 
    297        linkPref(subcheck, nested.name, "bool");
    298 
    299        subChecks.push(subcheck);
    300        subcheck.disabled = !pref._value;
    301        if (nested.shouldDisablePref) {
    302          subcheck.disabled = nested.shouldDisablePref;
    303        }
    304        subcheck.hidden = nested.hidden;
    305      }
    306    });
    307 
    308    // Special cases to like the nested prefs with another pref,
    309    // so we can disable it real time.
    310    if (id === "support-firefox") {
    311      function setupSupportFirefoxSubCheck(triggerPref, subPref) {
    312        const subCheckFullName = `browser.newtabpage.activity-stream.${triggerPref}`;
    313        const subCheckPref = Preferences.get(subCheckFullName);
    314 
    315        subCheckPref?.on("change", () => {
    316          const showSponsoredFullName = `browser.newtabpage.activity-stream.${subPref}`;
    317          const showSponsoredSubcheck = subChecks.find(
    318            subcheck =>
    319              subcheck.getAttribute("preference") === showSponsoredFullName
    320          );
    321          if (showSponsoredSubcheck) {
    322            showSponsoredSubcheck.disabled = !Services.prefs.getBoolPref(
    323              subCheckFullName,
    324              true
    325            );
    326          }
    327        });
    328      }
    329 
    330      setupSupportFirefoxSubCheck("feeds.section.topstories", "showSponsored");
    331      setupSupportFirefoxSubCheck("feeds.topsites", "showSponsoredTopSites");
    332    }
    333 
    334    pref.on("change", () => {
    335      subChecks.forEach(subcheck => {
    336        // Update child preferences for the "Support Firefox" checkbox group
    337        // so that they're turned on and off at the same time.
    338        if (id === "support-firefox") {
    339          const subPref = Preferences.get(subcheck.getAttribute("preference"));
    340          subPref.value = pref.value;
    341        }
    342 
    343        // Disable any nested checkboxes if the parent pref is not enabled.
    344        subcheck.disabled = !pref._value;
    345      });
    346    });
    347  }
    348 
    349  /**
    350   * Update the visibility of the Restore Defaults button based on checked prefs.
    351   *
    352   * @param gHomePane
    353   */
    354  toggleRestoreDefaults(gHomePane) {
    355    gHomePane.toggleRestoreDefaultsBtn();
    356  }
    357 
    358  /**
    359   * A helper function to append XUL elements on the page.
    360   *
    361   * @param document
    362   * @param tag
    363   * @param parent
    364   * @param options
    365   */
    366  createAppend(document, tag, parent, options = {}) {
    367    return parent.appendChild(document.createXULElement(tag, options));
    368  }
    369 
    370  /**
    371   * Helper to get fluentIDs sometimes encase in an object
    372   *
    373   * @param message
    374   * @returns string
    375   */
    376  getString(message) {
    377    return typeof message !== "object" ? message : message.id;
    378  }
    379 }