tor-browser

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

ActivityStream.sys.mjs (51141B)


      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 // We use importESModule here instead of static import so that
      6 // the Karma test environment won't choke on this module. This
      7 // is because the Karma test environment already stubs out
      8 // AppConstants, and overrides importESModule to be a no-op (which
      9 // can't be done for a static import statement).
     10 
     11 // eslint-disable-next-line mozilla/use-static-import
     12 const { AppConstants } = ChromeUtils.importESModule(
     13  "resource://gre/modules/AppConstants.sys.mjs"
     14 );
     15 
     16 // eslint-disable-next-line mozilla/use-static-import
     17 const { XPCOMUtils } = ChromeUtils.importESModule(
     18  "resource://gre/modules/XPCOMUtils.sys.mjs"
     19 );
     20 
     21 const lazy = {};
     22 
     23 ChromeUtils.defineESModuleGetters(lazy, {
     24  AboutNewTabParent: "resource:///actors/AboutNewTabParent.sys.mjs",
     25  AboutPreferences: "resource://newtab/lib/AboutPreferences.sys.mjs",
     26  AdsFeed: "resource://newtab/lib/AdsFeed.sys.mjs",
     27  InferredPersonalizationFeed:
     28    "resource://newtab/lib/InferredPersonalizationFeed.sys.mjs",
     29  SmartShortcutsFeed: "resource://newtab/lib/SmartShortcutsFeed.sys.mjs",
     30  DEFAULT_SITES: "resource://newtab/lib/DefaultSites.sys.mjs",
     31  DefaultPrefs: "resource://newtab/lib/ActivityStreamPrefs.sys.mjs",
     32  DiscoveryStreamFeed: "resource://newtab/lib/DiscoveryStreamFeed.sys.mjs",
     33  ExternalComponentsFeed:
     34    "resource://newtab/lib/ExternalComponentsFeed.sys.mjs",
     35  FaviconFeed: "resource://newtab/lib/FaviconFeed.sys.mjs",
     36  HighlightsFeed: "resource://newtab/lib/HighlightsFeed.sys.mjs",
     37  ListsFeed: "resource://newtab/lib/Widgets/ListsFeed.sys.mjs",
     38  NewTabAttributionFeed: "resource://newtab/lib/NewTabAttributionFeed.sys.mjs",
     39  NewTabActorRegistry: "resource://newtab/lib/NewTabActorRegistry.sys.mjs",
     40  NewTabInit: "resource://newtab/lib/NewTabInit.sys.mjs",
     41  NewTabMessaging: "resource://newtab/lib/NewTabMessaging.sys.mjs",
     42  NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
     43  PrefsFeed: "resource://newtab/lib/PrefsFeed.sys.mjs",
     44  PlacesFeed: "resource://newtab/lib/PlacesFeed.sys.mjs",
     45  RecommendationProvider:
     46    "resource://newtab/lib/RecommendationProvider.sys.mjs",
     47  Region: "resource://gre/modules/Region.sys.mjs",
     48  SectionsFeed: "resource://newtab/lib/SectionsManager.sys.mjs",
     49  StartupCacheInit: "resource://newtab/lib/StartupCacheInit.sys.mjs",
     50  Store: "resource://newtab/lib/Store.sys.mjs",
     51  SystemTickFeed: "resource://newtab/lib/SystemTickFeed.sys.mjs",
     52  TelemetryFeed: "resource://newtab/lib/TelemetryFeed.sys.mjs",
     53  TimerFeed: "resource://newtab/lib/Widgets/TimerFeed.sys.mjs",
     54  TopSitesFeed: "resource://newtab/lib/TopSitesFeed.sys.mjs",
     55  TopStoriesFeed: "resource://newtab/lib/TopStoriesFeed.sys.mjs",
     56  WallpaperFeed: "resource://newtab/lib/Wallpapers/WallpaperFeed.sys.mjs",
     57  WeatherFeed: "resource://newtab/lib/WeatherFeed.sys.mjs",
     58 });
     59 
     60 XPCOMUtils.defineLazyServiceGetter(
     61  lazy,
     62  "ProxyService",
     63  "@mozilla.org/network/protocol-proxy-service;1",
     64  Ci.nsIProtocolProxyService
     65 );
     66 
     67 // NB: Eagerly load modules that will be loaded/constructed/initialized in the
     68 // common case to avoid the overhead of wrapping and detecting lazy loading.
     69 import {
     70  actionCreators as ac,
     71  actionTypes as at,
     72 } from "resource://newtab/common/Actions.mjs";
     73 
     74 const REGION_INFERRED_PERSONALIZATION_CONFIG =
     75  "browser.newtabpage.activity-stream.discoverystream.sections.personalization.inferred.region-config";
     76 const LOCALE_INFERRED_PERSONALIZATION_CONFIG =
     77  "browser.newtabpage.activity-stream.discoverystream.sections.personalization.inferred.locale-config";
     78 const REGION_SOV_CONFIG =
     79  "browser.newtabpage.activity-stream.sov.region-config";
     80 const LOCALE_SOV_CONFIG =
     81  "browser.newtabpage.activity-stream.sov.locale-config";
     82 
     83 const REGION_WEATHER_CONFIG =
     84  "browser.newtabpage.activity-stream.discoverystream.region-weather-config";
     85 const LOCALE_WEATHER_CONFIG =
     86  "browser.newtabpage.activity-stream.discoverystream.locale-weather-config";
     87 
     88 const REGION_TOPICS_CONFIG =
     89  "browser.newtabpage.activity-stream.discoverystream.topicSelection.region-topics-config";
     90 const LOCALE_TOPICS_CONFIG =
     91  "browser.newtabpage.activity-stream.discoverystream.topicSelection.locale-topics-config";
     92 
     93 const REGION_TOPIC_LABEL_CONFIG =
     94  "browser.newtabpage.activity-stream.discoverystream.topicLabels.region-topic-label-config";
     95 const LOCALE_TOPIC_LABEL_CONFIG =
     96  "browser.newtabpage.activity-stream.discoverystream.topicLabels.locale-topic-label-config";
     97 const REGION_BASIC_CONFIG =
     98  "browser.newtabpage.activity-stream.discoverystream.region-basic-config";
     99 
    100 const REGION_CONTEXTUAL_AD_CONFIG =
    101  "browser.newtabpage.activity-stream.discoverystream.sections.contextualAds.region-config";
    102 const LOCALE_CONTEXTUAL_AD_CONFIG =
    103  "browser.newtabpage.activity-stream.discoverystream.sections.contextualAds.locale-config";
    104 
    105 const REGION_SECTIONS_CONFIG =
    106  "browser.newtabpage.activity-stream.discoverystream.sections.region-content-config";
    107 const LOCALE_SECTIONS_CONFIG =
    108  "browser.newtabpage.activity-stream.discoverystream.sections.locale-content-config";
    109 
    110 const PREF_SHOULD_AS_INITIALIZE_FEEDS =
    111  "browser.newtabpage.activity-stream.testing.shouldInitializeFeeds";
    112 
    113 const PREF_INFERRED_ENABLED =
    114  "discoverystream.sections.personalization.inferred.enabled";
    115 
    116 const PREF_IMAGE_PROXY_ENABLED =
    117  "browser.newtabpage.activity-stream.discoverystream.imageProxy.enabled";
    118 
    119 const PREF_IMAGE_PROXY_ENABLED_STORE = "discoverystream.imageProxy.enabled";
    120 
    121 const PREF_SHOULD_ENABLE_EXTERNAL_COMPONENTS_FEED =
    122  "browser.newtabpage.activity-stream.externalComponents.enabled";
    123 
    124 export const WEATHER_OPTIN_REGIONS = [
    125  "AT", // Austria
    126  "BE", // Belgium
    127  "BG", // Bulgaria
    128  "HR", // Croatia
    129  "CY", // Cyprus
    130  "CZ", // Czechia
    131  "DK", // Denmark
    132  "EE", // Estonia
    133  "FI", // Finland
    134  "FR", // France
    135  "DE", // Germany
    136  "GB", // United Kingdom
    137  "GR", // Greece
    138  "HU", // Hungary
    139  "IS", // Iceland
    140  "IE", // Ireland
    141  "IT", // Italy
    142  "LV", // Latvia
    143  "LI", // Liechtenstein
    144  "LT", // Lithuania
    145  "MT", // Malta
    146  "NL", // Netherlands
    147  "NO", // Norway
    148  "PL", // Poland
    149  "PT", // Portugal
    150  "RO", // Romania
    151  "SG", // Singapore
    152  "SK", // Slovakia
    153  "SI", // Slovenia
    154  "ES", // Spain
    155  "SE", // Sweden
    156  "CH", // Switzerland
    157 ];
    158 
    159 export function csvPrefHasValue(stringPrefName, value) {
    160  if (typeof stringPrefName !== "string") {
    161    throw new Error(`The stringPrefName argument is not a string`);
    162  }
    163 
    164  const pref = Services.prefs.getStringPref(stringPrefName, "") || "";
    165  const prefValues = pref
    166    .split(",")
    167    .map(s => s.trim())
    168    .filter(item => item);
    169 
    170  return prefValues.includes(value);
    171 }
    172 
    173 export function shouldInitializeFeeds(defaultValue = true) {
    174  // For tests/automation: when false, newtab won't initialize
    175  // select feeds in this session.
    176  // Flipping after initialization has no effect on the current session.
    177  const shouldInitialize = Services.prefs.getBoolPref(
    178    PREF_SHOULD_AS_INITIALIZE_FEEDS,
    179    defaultValue
    180  );
    181  return shouldInitialize;
    182 }
    183 
    184 function useInferredPersonalization({ geo, locale }) {
    185  return (
    186    csvPrefHasValue(REGION_INFERRED_PERSONALIZATION_CONFIG, geo) &&
    187    csvPrefHasValue(LOCALE_INFERRED_PERSONALIZATION_CONFIG, locale)
    188  );
    189 }
    190 
    191 function useSov({ geo, locale }) {
    192  return (
    193    csvPrefHasValue(REGION_SOV_CONFIG, geo) &&
    194    csvPrefHasValue(LOCALE_SOV_CONFIG, locale)
    195  );
    196 }
    197 
    198 function useContextualAds({ geo, locale }) {
    199  return (
    200    csvPrefHasValue(REGION_CONTEXTUAL_AD_CONFIG, geo) &&
    201    csvPrefHasValue(LOCALE_CONTEXTUAL_AD_CONFIG, locale)
    202  );
    203 }
    204 
    205 // Determine if spocs should be shown for a geo/locale
    206 function showSpocs({ geo }) {
    207  const spocsGeoString =
    208    lazy.NimbusFeatures.pocketNewtab.getVariable("regionSpocsConfig") || "";
    209  const spocsGeo = spocsGeoString.split(",").map(s => s.trim());
    210  return spocsGeo.includes(geo);
    211 }
    212 
    213 function showWeather({ geo, locale }) {
    214  return (
    215    csvPrefHasValue(REGION_WEATHER_CONFIG, geo) &&
    216    csvPrefHasValue(LOCALE_WEATHER_CONFIG, locale)
    217  );
    218 }
    219 
    220 function showWeatherOptIn({ geo }) {
    221  return WEATHER_OPTIN_REGIONS.includes(geo);
    222 }
    223 
    224 function showTopicsSelection({ geo, locale }) {
    225  return (
    226    csvPrefHasValue(REGION_TOPICS_CONFIG, geo) &&
    227    csvPrefHasValue(LOCALE_TOPICS_CONFIG, locale)
    228  );
    229 }
    230 
    231 function showTopicLabels({ geo, locale }) {
    232  return (
    233    csvPrefHasValue(REGION_TOPIC_LABEL_CONFIG, geo) &&
    234    csvPrefHasValue(LOCALE_TOPIC_LABEL_CONFIG, locale)
    235  );
    236 }
    237 
    238 function showSectionLayout({ geo, locale }) {
    239  return (
    240    csvPrefHasValue(REGION_SECTIONS_CONFIG, geo) &&
    241    csvPrefHasValue(LOCALE_SECTIONS_CONFIG, locale)
    242  );
    243 }
    244 
    245 // Configure default Activity Stream prefs with a plain `value` or a `getValue`
    246 // that computes a value. A `value_local_dev` is used for development defaults.
    247 export const PREFS_CONFIG = new Map([
    248  [
    249    "default.sites",
    250    {
    251      title:
    252        "Comma-separated list of default top sites to fill in behind visited sites",
    253      getValue: ({ geo }) =>
    254        lazy.DEFAULT_SITES.get(lazy.DEFAULT_SITES.has(geo) ? geo : ""),
    255    },
    256  ],
    257  [
    258    "feeds.topsites",
    259    {
    260      title: "Displays Top Sites on the New Tab Page",
    261      value: true,
    262    },
    263  ],
    264  [
    265    "hideTopSitesTitle",
    266    {
    267      title:
    268        "Hide the top sites section's title, including the section and collapse icons",
    269      value: false,
    270    },
    271  ],
    272  [
    273    "showSponsored",
    274    {
    275      title: "User pref for sponsored Pocket content",
    276      value: true,
    277    },
    278  ],
    279  [
    280    "system.showSponsored",
    281    {
    282      title: "System pref for sponsored Pocket content",
    283      // This pref is dynamic as the sponsored content depends on the region
    284      getValue: showSpocs,
    285    },
    286  ],
    287  [
    288    "showSponsoredTopSites",
    289    {
    290      title: "Show sponsored top sites",
    291      value: true,
    292    },
    293  ],
    294  [
    295    "mobileDownloadModal.enabled",
    296    {
    297      title: "Boolean flag to show download Firefox for mobile QR code modal",
    298      value: false,
    299    },
    300  ],
    301  [
    302    "mobileDownloadModal.variant-a",
    303    {
    304      title:
    305        "Boolean flag to turn download Firefox for mobile promo variant A on and off",
    306      value: false,
    307    },
    308  ],
    309  [
    310    "mobileDownloadModal.variant-b",
    311    {
    312      title:
    313        "Boolean flag to turn download Firefox for mobile promo variant B on and off",
    314      value: false,
    315    },
    316  ],
    317  [
    318    "mobileDownloadModal.variant-c",
    319    {
    320      title:
    321        "Boolean flag to turn download Firefox for mobile promo variant C on and off",
    322      value: false,
    323    },
    324  ],
    325  [
    326    "discoverystream.refinedCardsLayout.enabled",
    327    {
    328      title:
    329        "Boolean flag enable layout and styling refinements for content and ad cards across different card sizes",
    330      value: false,
    331    },
    332  ],
    333  [
    334    "discoverystream.merino-provider.ohttp.enabled",
    335    {
    336      title: "Enables the Merino requests and images sent over OHTTP",
    337      value: false,
    338    },
    339  ],
    340  [
    341    "unifiedAds.ohttp.enabled",
    342    {
    343      title: "Enables the MARS requests and images sent over OHTTP",
    344      value: false,
    345    },
    346  ],
    347  [
    348    "unifiedAds.adsFeed.enabled",
    349    {
    350      title:
    351        "Use AdsFeed.sys.mjs to fetch/cache/serve Mozilla Ad Routing Service (MARS) unified ads ",
    352      value: false,
    353    },
    354  ],
    355  [
    356    "unifiedAds.tiles.enabled",
    357    {
    358      title:
    359        "Use Mozilla Ad Routing Service (MARS) unified ads API for sponsored top sites tiles",
    360      value: false,
    361    },
    362  ],
    363  [
    364    "unifiedAds.spocs.enabled",
    365    {
    366      title:
    367        "Use Mozilla Ad Routing Service (MARS) unified ads API for sponsored content in recommended stories",
    368      value: false,
    369    },
    370  ],
    371  [
    372    "unifiedAds.endpoint",
    373    {
    374      title: "Mozilla Ad Routing Service (MARS) unified ads API endpoint URL",
    375      value: "https://ads.mozilla.org/",
    376    },
    377  ],
    378  [
    379    "unifiedAds.blockedAds",
    380    {
    381      title:
    382        "CSV list of blocked (dismissed) MARS ads. This payload is sent back every time new ads are fetched.",
    383      value: "",
    384    },
    385  ],
    386  [
    387    "system.showWeather",
    388    {
    389      title: "system.showWeather",
    390      // pref is dynamic
    391      getValue: showWeather,
    392    },
    393  ],
    394  [
    395    "showWeather",
    396    {
    397      title: "showWeather",
    398      value: true,
    399    },
    400  ],
    401  [
    402    "system.showWeatherOptIn",
    403    {
    404      title: "system.showWeatherOptIn",
    405      // pref is dynamic
    406      getValue: showWeatherOptIn,
    407    },
    408  ],
    409  [
    410    "discoverystream.optIn-region-weather-config",
    411    {
    412      title: "Regions for weather opt-in.",
    413      value: "DE,GB,FR,ES,IT,CH,AT,BE,IE,NL,PL,CZ,SE,SG,HU,SK,FI,DK,NO,PT",
    414    },
    415  ],
    416  [
    417    "weather.optInDisplayed",
    418    {
    419      title:
    420        "Enable opt-in dialog to display for weather widget in GDPR regions.",
    421      value: true,
    422    },
    423  ],
    424  [
    425    "weather.optInAccepted",
    426    {
    427      title:
    428        "User choice made when prompted with the opt-in dialog for weather.",
    429      value: false,
    430    },
    431  ],
    432  [
    433    "weather.staticData.enabled",
    434    {
    435      title:
    436        "Static weather data shown when user has not set/enabled location from opt-in.",
    437      value: true,
    438    },
    439  ],
    440  [
    441    "weather.query",
    442    {
    443      title: "weather.query",
    444      value: "",
    445    },
    446  ],
    447  [
    448    "weather.locationSearchEnabled",
    449    {
    450      title: "Enable the option to search for a specific city",
    451      value: false,
    452    },
    453  ],
    454  [
    455    "weather.temperatureUnits",
    456    {
    457      title: "Switch the temperature between Celsius and Fahrenheit",
    458      getValue: ({ geo }) => (geo === "US" ? "f" : "c"),
    459    },
    460  ],
    461  [
    462    "weather.display",
    463    {
    464      title:
    465        "Toggle the weather widget to include a text summary of the current conditions",
    466      value: "simple",
    467    },
    468  ],
    469  [
    470    "weather.placement",
    471    {
    472      title:
    473        "weather widget can be rendered in a variety of positions. Either in `header` or `sections`",
    474      value: "header",
    475    },
    476  ],
    477  [
    478    "images.smart",
    479    {
    480      title: "Smart crop images on newtab",
    481      value: false,
    482    },
    483  ],
    484  [
    485    "pocketCta",
    486    {
    487      title: "Pocket cta and button for logged out users.",
    488      value: JSON.stringify({
    489        cta_button: "",
    490        cta_text: "",
    491        cta_url: "",
    492        use_cta: false,
    493      }),
    494    },
    495  ],
    496  [
    497    "showSearch",
    498    {
    499      title: "Show the Search bar",
    500      value: true,
    501    },
    502  ],
    503  [
    504    "logowordmark.alwaysVisible",
    505    {
    506      title: "Show the logo and wordmark",
    507      value: true,
    508    },
    509  ],
    510  [
    511    "topSitesRows",
    512    {
    513      title: "Number of rows of Top Sites to display",
    514      value: 1,
    515    },
    516  ],
    517  [
    518    "telemetry",
    519    {
    520      title: "Enable system error and usage data collection",
    521      value: true,
    522      value_local_dev: false,
    523    },
    524  ],
    525  [
    526    "telemetry.ut.events",
    527    {
    528      title: "Enable Unified Telemetry event data collection",
    529      value: AppConstants.EARLY_BETA_OR_EARLIER,
    530      value_local_dev: false,
    531    },
    532  ],
    533  [
    534    "telemetry.structuredIngestion.endpoint",
    535    {
    536      title: "Structured Ingestion telemetry server endpoint",
    537      value: "https://incoming.telemetry.mozilla.org/submit",
    538    },
    539  ],
    540  [
    541    "telemetry.privatePing.enabled",
    542    {
    543      title: "Enables the private ping sent over OHTTP through Glean",
    544      value: false,
    545    },
    546  ],
    547  [
    548    "telemetry.surfaceId",
    549    {
    550      title: "surface id",
    551    },
    552  ],
    553  [
    554    "telemetry.privatePing.redactNewtabPing.enabled",
    555    {
    556      title: "Redacts content interaction ids from original New Tab ping",
    557      value: false,
    558    },
    559  ],
    560  [
    561    "telemetry.privatePing.inferredInterests.enabled",
    562    {
    563      title:
    564        "Includes interest vector with private ping when user has enabeled inferred personalization",
    565      value: false,
    566    },
    567  ],
    568  [
    569    "section.highlights.includeVisited",
    570    {
    571      title:
    572        "Boolean flag that decides whether or not to show visited pages in highlights.",
    573      value: true,
    574    },
    575  ],
    576  [
    577    "section.highlights.includeBookmarks",
    578    {
    579      title:
    580        "Boolean flag that decides whether or not to show bookmarks in highlights.",
    581      value: true,
    582    },
    583  ],
    584  [
    585    "section.highlights.includeDownloads",
    586    {
    587      title:
    588        "Boolean flag that decides whether or not to show saved recent Downloads in highlights.",
    589      value: true,
    590    },
    591  ],
    592  [
    593    "section.highlights.rows",
    594    {
    595      title: "Number of rows of Highlights to display",
    596      value: 1,
    597    },
    598  ],
    599  [
    600    "section.topstories.rows",
    601    {
    602      title: "Number of rows of Top Stories to display",
    603      value: 1,
    604    },
    605  ],
    606  [
    607    "sectionOrder",
    608    {
    609      title: "The rendering order for the sections",
    610      value: "topsites,topstories,highlights",
    611    },
    612  ],
    613  [
    614    "newtabWallpapers.enabled",
    615    {
    616      title: "Boolean flag to turn wallpaper functionality on and off",
    617      value: false,
    618    },
    619  ],
    620  [
    621    "newtabWallpapers.customColor.enabled",
    622    {
    623      title: "Boolean flag to turn show custom color select box",
    624      value: false,
    625    },
    626  ],
    627  [
    628    "newtabWallpapers.customWallpaper.enabled",
    629    {
    630      title:
    631        "Boolean flag to enable custom/user-uploaded wallpaper functionality",
    632      value: false,
    633    },
    634  ],
    635  [
    636    "newtabWallpapers.customWallpaper.uuid",
    637    {
    638      title: "uuid for uploaded custom wallpaper",
    639      value: "",
    640    },
    641  ],
    642  [
    643    "newtabWallpapers.customWallpaper.uploadedPreviously",
    644    {
    645      title:
    646        "Boolean flag used for telemetry to track if a user has previously uploaded a custom wallpaper",
    647      value: false,
    648    },
    649  ],
    650  [
    651    "newtabWallpapers.customWallpaper.fileSize.enabled",
    652    {
    653      title: "Boolean flag to enforce a maximum file size for uploaded images",
    654      value: false,
    655    },
    656  ],
    657  [
    658    "newtabWallpapers.customWallpaper.fileSize",
    659    {
    660      title: "Number pref of maximum file size (in MB) a user can upload",
    661      value: 0,
    662    },
    663  ],
    664  [
    665    "newtabWallpapers.customWallpaper.theme",
    666    {
    667      title: "theme ('light' | 'dark') of user uploaded wallpaper",
    668      value: "",
    669    },
    670  ],
    671  [
    672    "newtabAdSize.leaderboard",
    673    {
    674      title: "Boolean flag to turn the leaderboard ad size on and off",
    675      value: false,
    676    },
    677  ],
    678  [
    679    "newtabAdSize.leaderboard.position",
    680    {
    681      title:
    682        "position for leaderboard spoc - should correlate to a row in DS grid",
    683      value: "3",
    684    },
    685  ],
    686  [
    687    "newtabAdSize.billboard",
    688    {
    689      title: "Boolean flag to turn the billboard ad size on and off",
    690      value: false,
    691    },
    692  ],
    693  [
    694    "newtabAdSize.billboard.position",
    695    {
    696      title:
    697        "position for billboard spoc - should correlate to a row in DS grid",
    698      value: "3",
    699    },
    700  ],
    701  [
    702    "newtabAdSize.mediumRectangle",
    703    {
    704      title: "Boolean flag to turn the medium (MREC) ad size on and off",
    705      value: false,
    706    },
    707  ],
    708  [
    709    "discoverystream.promoCard.enabled",
    710    {
    711      title: "Boolean flag to turn the promo card on and off",
    712      value: false,
    713    },
    714  ],
    715  [
    716    "discoverystream.promoCard.visible",
    717    {
    718      title: "Boolean flag whether the promo card is visible or not",
    719      value: false,
    720    },
    721  ],
    722  [
    723    "discoverystream.sections.enabled",
    724    {
    725      title: "Boolean flag to enable section layout UI in recommended stories",
    726      getValue: showSectionLayout,
    727    },
    728  ],
    729  [
    730    "discoverystream.sections.personalization.enabled",
    731    {
    732      title:
    733        "Boolean flag to enable personalized sections layout. Allows users to follow/unfollow topic sections.",
    734      value: false,
    735    },
    736  ],
    737  [
    738    "discoverystream.sections.customizeMenuPanel.enabled",
    739    {
    740      title:
    741        "Boolean flag to enable the setions management panel in Customize menu",
    742      value: false,
    743    },
    744  ],
    745  [
    746    "discoverystream.sections.cards.enabled",
    747    {
    748      title:
    749        "Boolean flag to enable revised pocket story card UI in recommended stories",
    750      value: false,
    751    },
    752  ],
    753  [
    754    "discoverystream.sections.contextualAds.enabled",
    755    {
    756      title: "Boolean flag to enable contextual ads",
    757      getValue: useContextualAds,
    758    },
    759  ],
    760  [
    761    "discoverystream.sections.personalization.inferred.enabled",
    762    {
    763      title: "Boolean flag to enable inferred personalizaton",
    764      // pref is dynamic
    765      getValue: useInferredPersonalization,
    766    },
    767  ],
    768  [
    769    "discoverystream.sections.personalization.inferred.interests.override",
    770    {
    771      title:
    772        "Testing feature to allow specification of specific user interests",
    773    },
    774  ],
    775  [
    776    "discoverystream.dailyBrief.enabled",
    777    {
    778      title: "Boolean flag to enable the daily brief section",
    779      value: false,
    780    },
    781  ],
    782  [
    783    "discoverystream.dailyBrief.sectionId",
    784    {
    785      title: "sectionId for the Daily brief section",
    786      value: "top_stories_section",
    787    },
    788  ],
    789  [
    790    "discoverystream.shortcuts.personalization.enabled",
    791    {
    792      title: "Boolean flag to enable shortcuts personalization",
    793      value: false,
    794    },
    795  ],
    796  [
    797    "discoverystream.shortcuts.force_log.enabled",
    798    {
    799      title:
    800        "Boolean flag to enable logging shortcuts interactions even if enabled is off",
    801      value: false,
    802    },
    803  ],
    804  [
    805    "discoverystream.attribution.enabled",
    806    {
    807      title: "Boolean flag to enable newtab attribution",
    808      value: false,
    809    },
    810  ],
    811  [
    812    "discoverystream.sections.personalization.inferred.user.enabled",
    813    {
    814      title: "User pref to toggle inferred personalizaton",
    815      value: false,
    816    },
    817  ],
    818  [
    819    "discoverystream.sections.personalization.inferred.model.override",
    820    {
    821      title:
    822        "Override inferred personalization model JSON string that typically comes from rec API. Or 'TEST' for a test model",
    823    },
    824  ],
    825  [
    826    "discoverystream.reportAds.enabled",
    827    {
    828      title: "Boolean flag to enable reporting ads from the context menu",
    829      value: false,
    830    },
    831  ],
    832  [
    833    "discoverystream.sections.following",
    834    {
    835      title: "A comma-separated list of strings of followed section topics",
    836      value: "",
    837    },
    838  ],
    839  [
    840    "discoverystream.sections.blocked",
    841    {
    842      title: "A comma-separated list of strings of blocked section topics",
    843      value: "",
    844    },
    845  ],
    846  [
    847    "discoverystream.sections.interestPicker.enabled",
    848    {
    849      title: "Boolean flag to enable the inline interest picker",
    850      value: false,
    851    },
    852  ],
    853  [
    854    "discoverystream.sections.interestPicker.visibleSections",
    855    {
    856      title: "comma separated string of sections that are visible",
    857      value: "",
    858    },
    859  ],
    860  [
    861    "discoverystream.spoc-positions",
    862    {
    863      title: "CSV string of spoc position indexes on newtab Pocket grid",
    864      value: "1,5,7,11,18,20",
    865    },
    866  ],
    867  [
    868    "discoverystream.placements.contextualSpocs",
    869    {
    870      title:
    871        "CSV string of spoc placement ids on newtab Pocket grid. A placement id tells our ad server where the ads are intended to be displayed.",
    872    },
    873  ],
    874  [
    875    "discoverystream.placements.contextualSpocs.counts",
    876    {
    877      title:
    878        "CSV string of spoc placement counts on newtab Pocket grid. The count tells the ad server how many ads to return for this position and placement.",
    879    },
    880  ],
    881  [
    882    "discoverystream.placements.contextualBanners",
    883    {
    884      title:
    885        "CSV string of the banner placement ids on newtab Pocket grid. This placement id tells us which banner is visible when contexual ads are on",
    886      value: "",
    887    },
    888  ],
    889  [
    890    "discoverystream.placements.contextualBanners.counts",
    891    {
    892      title:
    893        "CSV string of AdBanner placement counts on newtab Pocket grid. The count tells the ad server how many banners to return for this position and placement.",
    894      value: "",
    895    },
    896  ],
    897  [
    898    "discoverystream.placements.spocs",
    899    {
    900      title:
    901        "CSV string of spoc placement ids on newtab Pocket grid. A placement id tells our ad server where the ads are intended to be displayed.",
    902    },
    903  ],
    904  [
    905    "discoverystream.placements.spocs.counts",
    906    {
    907      title:
    908        "CSV string of spoc placement counts on newtab Pocket grid. The count tells the ad server how many ads to return for this position and placement.",
    909    },
    910  ],
    911  [
    912    "discoverystream.placements.tiles",
    913    {
    914      title:
    915        "CSV string of tiles placement ids on newtab tiles section. A placement id tells our ad server where the ads are intended to be displayed.",
    916    },
    917  ],
    918  [
    919    "discoverystream.placements.tiles.counts",
    920    {
    921      title:
    922        "CSV string of tiles placement counts on newtab tiles section. The count tells the ad server how many ads to return for this position and placement.",
    923    },
    924  ],
    925  [
    926    "discoverystream.imageProxy.enabled",
    927    {
    928      title: "Boolean flag to enable image proxying for images on newtab",
    929      value: false,
    930    },
    931  ],
    932  [
    933    "newtabWallpapers.highlightEnabled",
    934    {
    935      title: "Boolean flag to show the highlight about the Wallpaper feature",
    936      value: false,
    937    },
    938  ],
    939  [
    940    "newtabWallpapers.highlightDismissed",
    941    {
    942      title:
    943        "Boolean flag to remember if the user has seen the feature highlight",
    944      value: false,
    945    },
    946  ],
    947  [
    948    "newtabWallpapers.highlightSeenCounter",
    949    {
    950      title: "Count the number of times a user has seen the feature highlight",
    951      value: 0,
    952    },
    953  ],
    954  [
    955    "newtabWallpapers.highlightHeaderText",
    956    {
    957      title: "Changes the wallpaper feature highlight header text",
    958      value: "",
    959    },
    960  ],
    961  [
    962    "newtabWallpapers.highlightContentText",
    963    {
    964      title: "Changes the wallpaper feature highlight content text",
    965      value: "",
    966    },
    967  ],
    968  [
    969    "newtabWallpapers.highlightCtaText",
    970    {
    971      title: "Changes the wallpaper feature highlight cta text",
    972      value: "",
    973    },
    974  ],
    975  [
    976    "newtabWallpapers.wallpaper",
    977    {
    978      title: "Currently set wallpaper",
    979      value: "",
    980    },
    981  ],
    982  [
    983    "sov.enabled",
    984    {
    985      title: "Enables share of voice (SOV)",
    986      getValue: useSov,
    987    },
    988  ],
    989  [
    990    "sov.name",
    991    {
    992      title:
    993        "A unique id, usually this is a timestamp for the day it was generated",
    994      value: "SOV-20251122215625",
    995    },
    996  ],
    997  [
    998    "sov.frecency.exposure",
    999    {
   1000      title:
   1001        "Is or was the user eligible for frecency ranked sponsored shortcuts",
   1002      value: false,
   1003    },
   1004  ],
   1005  [
   1006    "sov.amp.allocation",
   1007    {
   1008      title: "How many positions can be filled from amp",
   1009      value: "100, 100, 100",
   1010    },
   1011  ],
   1012  [
   1013    "sov.frecency.allocation",
   1014    {
   1015      title: "How many positions can be filled by frecency",
   1016      value: "0, 0, 0",
   1017    },
   1018  ],
   1019  [
   1020    "widgets.system.enabled",
   1021    {
   1022      title: "Enables visibility of all widgets and controls to enable them",
   1023      value: false,
   1024    },
   1025  ],
   1026  [
   1027    "widgets.enabled",
   1028    {
   1029      title: "Allows users to toggle all widgets on and off at once",
   1030      value: false,
   1031    },
   1032  ],
   1033  [
   1034    "widgets.lists.enabled",
   1035    {
   1036      title: "Enables the to-do lists widget",
   1037      value: true,
   1038    },
   1039  ],
   1040  [
   1041    "widgets.lists.maxLists",
   1042    {
   1043      title: "Maximum number of lists that can be created",
   1044      value: 10,
   1045    },
   1046  ],
   1047  [
   1048    "widgets.lists.maxListItems",
   1049    {
   1050      title:
   1051        "Maximum number of items that can be created on an individual list",
   1052      value: 100,
   1053    },
   1054  ],
   1055  [
   1056    "widgets.system.lists.enabled",
   1057    {
   1058      title: "Enables the to-do lists widget experiment in Nimbus",
   1059      value: false,
   1060    },
   1061  ],
   1062  [
   1063    "widgets.lists.interaction",
   1064    {
   1065      title:
   1066        "Boolean flag for determining if a user has interacted with the lists widget",
   1067      value: false,
   1068    },
   1069  ],
   1070  [
   1071    "widgets.lists.badge.enabled",
   1072    {
   1073      title: "Show badge on lists widget to indicate new/beta feature",
   1074      value: false,
   1075    },
   1076  ],
   1077  [
   1078    "widgets.lists.badge.label",
   1079    {
   1080      title: "Label type for lists widget badge (New or Beta)",
   1081      value: "",
   1082    },
   1083  ],
   1084  [
   1085    "widgets.maximized",
   1086    {
   1087      title: "Toggles maximized state for all widgets in the widgets section",
   1088      value: false,
   1089    },
   1090  ],
   1091  [
   1092    "widgets.system.maximized",
   1093    {
   1094      title: "Enables the maximize widget feature experiment in Nimbus",
   1095      value: false,
   1096    },
   1097  ],
   1098  [
   1099    "widgets.focusTimer.enabled",
   1100    {
   1101      title: "Enables the focus timer widget",
   1102      value: true,
   1103    },
   1104  ],
   1105  [
   1106    "widgets.system.focusTimer.enabled",
   1107    {
   1108      title: "Enables the focus timer widget experiment in Nimbus",
   1109      value: false,
   1110    },
   1111  ],
   1112  [
   1113    "widgets.focusTimer.interaction",
   1114    {
   1115      title:
   1116        "Boolean flag for determining if a user has interacted with the timer widget",
   1117      value: false,
   1118    },
   1119  ],
   1120  [
   1121    "widgets.focusTimer.showSystemNotifications",
   1122    {
   1123      title: "Enables the focus timer widget to show system notifications",
   1124      value: false,
   1125    },
   1126  ],
   1127  [
   1128    "widgets.weatherForecast.enabled",
   1129    {
   1130      title: "Enables the weather forecast widget",
   1131      value: true,
   1132    },
   1133  ],
   1134  [
   1135    "widgets.system.weatherForecast.enabled",
   1136    {
   1137      title: "Enables the weather forecast widget experiment in Nimbus",
   1138      value: false,
   1139    },
   1140  ],
   1141  [
   1142    "widgets.weatherForecast.interaction",
   1143    {
   1144      title:
   1145        "Boolean flag for determining if a user has interacted with the weather forecast widget",
   1146      value: false,
   1147    },
   1148  ],
   1149  [
   1150    "improvesearch.noDefaultSearchTile",
   1151    {
   1152      title: "Remove tiles that are the same as the default search",
   1153      value: true,
   1154    },
   1155  ],
   1156  [
   1157    "improvesearch.topSiteSearchShortcuts.searchEngines",
   1158    {
   1159      title:
   1160        "An ordered, comma-delimited list of search shortcuts that we should try and pin",
   1161      // This pref is dynamic as the shortcuts vary depending on the region
   1162      getValue: ({ geo }) => {
   1163        if (!geo) {
   1164          return "";
   1165        }
   1166        const searchShortcuts = [];
   1167        if (geo === "CN") {
   1168          searchShortcuts.push("baidu");
   1169        } else if (["BY", "KZ", "RU", "TR"].includes(geo)) {
   1170          searchShortcuts.push("yandex");
   1171        } else {
   1172          searchShortcuts.push("google");
   1173        }
   1174        if (["DE", "FR", "GB", "IT", "JP", "US"].includes(geo)) {
   1175          searchShortcuts.push("amazon");
   1176        }
   1177        return searchShortcuts.join(",");
   1178      },
   1179    },
   1180  ],
   1181  [
   1182    "improvesearch.topSiteSearchShortcuts.havePinned",
   1183    {
   1184      title:
   1185        "A comma-delimited list of search shortcuts that have previously been pinned",
   1186      value: "",
   1187    },
   1188  ],
   1189  [
   1190    "asrouter.devtoolsEnabled",
   1191    {
   1192      title: "Are the asrouter devtools enabled?",
   1193      value: false,
   1194    },
   1195  ],
   1196  [
   1197    "discoverystream.flight.blocks",
   1198    {
   1199      title: "Track flight blocks",
   1200      skipBroadcast: true,
   1201      value: "{}",
   1202    },
   1203  ],
   1204  [
   1205    "discoverystream.config",
   1206    {
   1207      title: "Configuration for the new pocket new tab",
   1208      getValue: () => {
   1209        return JSON.stringify({
   1210          collapsible: true,
   1211          enabled: true,
   1212        });
   1213      },
   1214    },
   1215  ],
   1216  [
   1217    "discoverystream.endpoints",
   1218    {
   1219      title:
   1220        "Endpoint prefixes (comma-separated) that are allowed to be requested",
   1221      value:
   1222        "https://getpocket.cdn.mozilla.net/,https://firefox-api-proxy.cdn.mozilla.net/,https://spocs.getpocket.com/,https://merino.services.mozilla.com/,https://ads.mozilla.org/",
   1223    },
   1224  ],
   1225  [
   1226    "discoverystream.region-basic-layout",
   1227    {
   1228      title: "Decision to use basic layout based on region.",
   1229      getValue: ({ geo }) => {
   1230        const preffedRegionsString =
   1231          Services.prefs.getStringPref(REGION_BASIC_CONFIG) || "";
   1232        // If no regions are set to basic,
   1233        // we don't need to bother checking against the region.
   1234        // We are also not concerned if geo is not set,
   1235        // because stories are going to be empty until we have geo.
   1236        if (!preffedRegionsString) {
   1237          return false;
   1238        }
   1239        const preffedRegions = preffedRegionsString
   1240          .split(",")
   1241          .map(s => s.trim());
   1242 
   1243        return preffedRegions.includes(geo);
   1244      },
   1245    },
   1246  ],
   1247  [
   1248    "discoverystream.spoc.impressions",
   1249    {
   1250      title: "Track spoc impressions",
   1251      skipBroadcast: true,
   1252      value: "{}",
   1253    },
   1254  ],
   1255  [
   1256    "discoverystream.endpointSpocsClear",
   1257    {
   1258      title:
   1259        "Endpoint for when a user opts-out of sponsored content to delete the user's data from the ad server.",
   1260      value: "https://spocs.getpocket.com/user",
   1261    },
   1262  ],
   1263  [
   1264    "discoverystream.rec.impressions",
   1265    {
   1266      title: "Track rec impressions",
   1267      skipBroadcast: true,
   1268      value: "{}",
   1269    },
   1270  ],
   1271  [
   1272    "discoverystream.topicSelection.enabled",
   1273    {
   1274      title: "Enables topic selection for discovery stream",
   1275      // pref is dynamic
   1276      getValue: showTopicsSelection,
   1277    },
   1278  ],
   1279  [
   1280    "discoverystream.topicSelection.topics",
   1281    {
   1282      title: "Topics available",
   1283      value:
   1284        "business, arts, food, health, finance, government, sports, tech, travel, education-science, society",
   1285    },
   1286  ],
   1287  [
   1288    "discoverystream.topicSelection.selectedTopics",
   1289    {
   1290      title: "Selected topics",
   1291      value: "",
   1292    },
   1293  ],
   1294  [
   1295    "discoverystream.topicSelection.suggestedTopics",
   1296    {
   1297      title: "Suggested topics to choose during onboarding for topic selection",
   1298      value: "business, arts, government",
   1299    },
   1300  ],
   1301  [
   1302    "discoverystream.topicSelection.hasBeenUpdatedPreviously",
   1303    {
   1304      title: "Returns true only if the user has previously selected topics",
   1305      value: false,
   1306    },
   1307  ],
   1308  [
   1309    "discoverystream.topicSelection.onboarding.displayCount",
   1310    {
   1311      title: "amount of times that topic selection onboarding has been shown",
   1312      value: 0,
   1313    },
   1314  ],
   1315  [
   1316    "discoverystream.topicSelection.onboarding.maybeDisplay",
   1317    {
   1318      title:
   1319        "Whether the onboarding should be shown, based on previous interactions",
   1320      value: true,
   1321    },
   1322  ],
   1323  [
   1324    "discoverystream.topicSelection.onboarding.lastDisplayed",
   1325    {
   1326      title:
   1327        "time in ms that onboarding was last shown (stored as string due to contraits of prefs)",
   1328      value: "",
   1329    },
   1330  ],
   1331  [
   1332    "discoverystream.topicSelection.onboarding.displayTimeout",
   1333    {
   1334      title: "time in ms that the onboarding show be shown next",
   1335      value: 0,
   1336    },
   1337  ],
   1338  [
   1339    "discoverystream.topicSelection.onboarding.enabled",
   1340    {
   1341      title: "enabled onboarding experience for topic selection onboarding",
   1342      value: false,
   1343    },
   1344  ],
   1345  [
   1346    "discoverystream.topicLabels.enabled",
   1347    {
   1348      title: "Enables topic labels for discovery stream",
   1349      // pref is dynamic
   1350      getValue: showTopicLabels,
   1351    },
   1352  ],
   1353  [
   1354    "discoverystream.spocs.onDemand",
   1355    {
   1356      title: "Set sponsored content to only update cache when requested.",
   1357      value: false,
   1358    },
   1359  ],
   1360  [
   1361    "discoverystream.spocs.cacheTimeout",
   1362    {
   1363      title: "Set sponsored content cache timeout in minutes.",
   1364    },
   1365  ],
   1366  [
   1367    "discoverystream.spocs.startupCache.enabled",
   1368    {
   1369      title: "Controls if spocs should be included in startup cache.",
   1370      value: false,
   1371    },
   1372  ],
   1373  [
   1374    "discoverystream.publisherFavicon.enabled",
   1375    {
   1376      title: "Enables publisher favicons on recommended stories",
   1377      value: false,
   1378    },
   1379  ],
   1380  [
   1381    "discoverystream.sections.clientLayout.enabled",
   1382    {
   1383      title: "Enables client side layout for recommended stories",
   1384      value: false,
   1385    },
   1386  ],
   1387  [
   1388    "support.url",
   1389    {
   1390      title: "Link to HNT's support page",
   1391      getValue: () => {
   1392        // Services.urlFormatter completes the in-product SUMO page URL:
   1393        // https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/new-tab
   1394        const baseUrl = Services.urlFormatter.formatURLPref(
   1395          "app.support.baseURL"
   1396        );
   1397        return `${baseUrl}new-tab`;
   1398      },
   1399    },
   1400  ],
   1401  [
   1402    "caretBlinkCount",
   1403    {
   1404      title:
   1405        "The amount of times the caret blinks. This pref copies the value from the system settings",
   1406      getValue: () => {
   1407        return Services.appinfo.caretBlinkCount;
   1408      },
   1409    },
   1410  ],
   1411  [
   1412    "caretBlinkTime",
   1413    {
   1414      title:
   1415        "Rate at which the caret blinks. This pref copies the value from the system settings",
   1416      getValue: () => {
   1417        return Services.appinfo.caretBlinkTime;
   1418      },
   1419    },
   1420  ],
   1421  [
   1422    "showSponsoredCheckboxes",
   1423    {
   1424      title:
   1425        "'Support Firefox' pref on 'about:settings#home' page. Toggles all sponsored results on and off at the same time",
   1426      value: true,
   1427    },
   1428  ],
   1429 ]);
   1430 
   1431 // Array of each feed's FEEDS_CONFIG factory and values to add to PREFS_CONFIG
   1432 const FEEDS_DATA = [
   1433  {
   1434    name: "aboutpreferences",
   1435    factory: () => new lazy.AboutPreferences(),
   1436    title: "about:preferences rendering",
   1437    value: true,
   1438  },
   1439  {
   1440    name: "newtabinit",
   1441    factory: () => new lazy.NewTabInit(),
   1442    title: "Sends a copy of the state to each new tab that is opened",
   1443    value: true,
   1444  },
   1445  {
   1446    name: "places",
   1447    factory: () => new lazy.PlacesFeed(),
   1448    title: "Listens for and relays various Places-related events",
   1449    value: true,
   1450  },
   1451  {
   1452    name: "prefs",
   1453    factory: () => new lazy.PrefsFeed(PREFS_CONFIG),
   1454    title: "Preferences",
   1455    value: true,
   1456  },
   1457  {
   1458    name: "sections",
   1459    factory: () => new lazy.SectionsFeed(),
   1460    title: "Manages sections",
   1461    value: true,
   1462  },
   1463  {
   1464    name: "startupcacheinit",
   1465    factory: () => new lazy.StartupCacheInit(),
   1466    title: "Sends a copy of the state to the startup cache newtab",
   1467    value: true,
   1468  },
   1469  {
   1470    name: "section.highlights",
   1471    factory: () => new lazy.HighlightsFeed(),
   1472    title: "Fetches content recommendations from places db",
   1473    value: false,
   1474  },
   1475  {
   1476    name: "system.topstories",
   1477    factory: () =>
   1478      new lazy.TopStoriesFeed(PREFS_CONFIG.get("discoverystream.config")),
   1479    title:
   1480      "System pref that fetches content recommendations from a configurable content provider",
   1481    // Dynamically determine if Pocket should be shown for a geo / locale
   1482    getValue: ({ geo, locale }) => {
   1483      // If we don't have geo, we don't want to flash the screen with stories while geo loads.
   1484      // Best to display nothing until geo is ready.
   1485      if (!geo) {
   1486        return false;
   1487      }
   1488      const preffedRegionsBlockString =
   1489        lazy.NimbusFeatures.pocketNewtab.getVariable("regionStoriesBlock") ||
   1490        "";
   1491      const preffedRegionsString =
   1492        lazy.NimbusFeatures.pocketNewtab.getVariable("regionStoriesConfig") ||
   1493        "";
   1494      const preffedLocaleListString =
   1495        lazy.NimbusFeatures.pocketNewtab.getVariable("localeListConfig") || "";
   1496      const preffedBlockRegions = preffedRegionsBlockString
   1497        .split(",")
   1498        .map(s => s.trim());
   1499      const preffedRegions = preffedRegionsString.split(",").map(s => s.trim());
   1500      const preffedLocales = preffedLocaleListString
   1501        .split(",")
   1502        .map(s => s.trim());
   1503      const locales = {
   1504        US: ["en-CA", "en-GB", "en-US"],
   1505        CA: ["en-CA", "en-GB", "en-US"],
   1506        GB: ["en-CA", "en-GB", "en-US"],
   1507        AU: ["en-CA", "en-GB", "en-US"],
   1508        NZ: ["en-CA", "en-GB", "en-US"],
   1509        IN: ["en-CA", "en-GB", "en-US"],
   1510        IE: ["en-CA", "en-GB", "en-US"],
   1511        ZA: ["en-CA", "en-GB", "en-US"],
   1512        CH: ["de"],
   1513        BE: ["de"],
   1514        DE: ["de"],
   1515        AT: ["de"],
   1516        IT: ["it"],
   1517        FR: ["fr"],
   1518        ES: ["es-ES"],
   1519        PL: ["pl"],
   1520        JP: ["ja", "ja-JP-mac"],
   1521      }[geo];
   1522 
   1523      const regionBlocked = preffedBlockRegions.includes(geo);
   1524      const localeEnabled = locale && preffedLocales.includes(locale);
   1525      const regionEnabled =
   1526        preffedRegions.includes(geo) && !!locales && locales.includes(locale);
   1527      return !regionBlocked && (localeEnabled || regionEnabled);
   1528    },
   1529  },
   1530  {
   1531    name: "systemtick",
   1532    factory: () => new lazy.SystemTickFeed(),
   1533    title: "Produces system tick events to periodically check for data expiry",
   1534    value: true,
   1535  },
   1536  {
   1537    name: "telemetry",
   1538    factory: () => new lazy.TelemetryFeed(),
   1539    title: "Relays telemetry-related actions to PingCentre",
   1540    value: true,
   1541  },
   1542  {
   1543    name: "favicon",
   1544    factory: () => new lazy.FaviconFeed(),
   1545    title: "Fetches tippy top manifests from remote service",
   1546    value: true,
   1547  },
   1548  {
   1549    name: "system.topsites",
   1550    factory: () => new lazy.TopSitesFeed(),
   1551    title: "Queries places and gets metadata for Top Sites section",
   1552    value: true,
   1553  },
   1554  {
   1555    name: "recommendationprovider",
   1556    factory: () => new lazy.RecommendationProvider(),
   1557    title: "Handles setup and interaction for the personality provider",
   1558    value: true,
   1559  },
   1560  {
   1561    name: "discoverystreamfeed",
   1562    factory: () => new lazy.DiscoveryStreamFeed(),
   1563    title: "Handles new pocket ui for the new tab page",
   1564    value: true,
   1565  },
   1566  {
   1567    name: "wallpaperfeed",
   1568    factory: () => new lazy.WallpaperFeed(),
   1569    title: "Handles fetching and managing wallpaper data from RemoteSettings",
   1570    value: true,
   1571  },
   1572  {
   1573    name: "weatherfeed",
   1574    factory: () => new lazy.WeatherFeed(),
   1575    title: "Handles fetching and caching weather data",
   1576    value: true,
   1577  },
   1578  {
   1579    name: "adsfeed",
   1580    factory: () => new lazy.AdsFeed(),
   1581    title: "Handles fetching and caching ads data",
   1582    value: true,
   1583  },
   1584  {
   1585    name: "inferredpersonalizationfeed",
   1586    factory: () => new lazy.InferredPersonalizationFeed(),
   1587    title:
   1588      "Handles generating and caching an interest vector for inferred personalization",
   1589    value: true,
   1590  },
   1591  {
   1592    name: "smartshortcutsfeed",
   1593    factory: () => new lazy.SmartShortcutsFeed(),
   1594    title:
   1595      "Handles generating and caching an interest vector for shortcuts personalization",
   1596    value: true,
   1597  },
   1598  {
   1599    name: "newtabattributionfeed",
   1600    factory: () => new lazy.NewTabAttributionFeed(),
   1601    title: "Handles a local DB for story and shortcuts clicks and impressions",
   1602    value: true,
   1603  },
   1604  {
   1605    name: "newtabmessaging",
   1606    factory: () => new lazy.NewTabMessaging(),
   1607    title: "Handles fetching and triggering ASRouter messages in newtab",
   1608    value: true,
   1609  },
   1610  {
   1611    name: "listsfeed",
   1612    factory: () => new lazy.ListsFeed(),
   1613    title: "Handles the data for the Todo list widget",
   1614    value: true,
   1615  },
   1616  {
   1617    name: "timerfeed",
   1618    factory: () => new lazy.TimerFeed(),
   1619    title: "Handles the data for the Timer widget",
   1620    value: true,
   1621  },
   1622  {
   1623    name: "externalcomponentsfeed",
   1624    factory: () => new lazy.ExternalComponentsFeed(),
   1625    title: "Handles updating the registry of external components",
   1626    getValue() {
   1627      // This feed should only be enabled on versions of the app that have the
   1628      // AboutNewTabComponents module. Those versions of the app have this
   1629      // preference set to true.
   1630      return Services.prefs.getBoolPref(
   1631        PREF_SHOULD_ENABLE_EXTERNAL_COMPONENTS_FEED,
   1632        false
   1633      );
   1634    },
   1635  },
   1636 ];
   1637 
   1638 const FEEDS_CONFIG = new Map();
   1639 
   1640 for (const config of FEEDS_DATA) {
   1641  const pref = `feeds.${config.name}`;
   1642  FEEDS_CONFIG.set(pref, config.factory);
   1643  PREFS_CONFIG.set(pref, config);
   1644 }
   1645 
   1646 export class ActivityStream {
   1647  /**
   1648   * constructor - Initializes an instance of ActivityStream
   1649   */
   1650  constructor() {
   1651    this.initialized = false;
   1652    this.store = new lazy.Store();
   1653    this._defaultPrefs = new lazy.DefaultPrefs(PREFS_CONFIG);
   1654    this._proxyRegistered = false;
   1655  }
   1656 
   1657  get feeds() {
   1658    if (shouldInitializeFeeds()) {
   1659      return FEEDS_CONFIG;
   1660    }
   1661 
   1662    // We currently make excpetions for topsites, and prefs feeds
   1663    // because they currently impacts tests timing for places initialization.
   1664    // See bug 1999166.
   1665    const feeds = new Map([
   1666      ["feeds.system.topsites", FEEDS_CONFIG.get("feeds.system.topsites")],
   1667      ["feeds.prefs", FEEDS_CONFIG.get("feeds.prefs")],
   1668    ]);
   1669    return feeds;
   1670  }
   1671 
   1672  init() {
   1673    this._updateDynamicPrefs();
   1674    this._defaultPrefs.init();
   1675    Services.obs.addObserver(this, "intl:app-locales-changed");
   1676    Services.prefs.addObserver(PREF_IMAGE_PROXY_ENABLED, this);
   1677    lazy.NewTabActorRegistry.init();
   1678 
   1679    // Hook up the store and let all feeds and pages initialize
   1680    this.store.init(
   1681      this.feeds,
   1682      ac.BroadcastToContent({
   1683        type: at.INIT,
   1684        data: {
   1685          locale: this.locale,
   1686        },
   1687        meta: {
   1688          isStartup: true,
   1689        },
   1690      }),
   1691      { type: at.UNINIT }
   1692    );
   1693 
   1694    this.initialized = true;
   1695 
   1696    this.registerNetworkProxy();
   1697  }
   1698 
   1699  /**
   1700   * Registers network proxy channel filter for image requests.
   1701   * This enables privacy-preserving image proxy for newtab when
   1702   * inferred personalization is enabled.
   1703   */
   1704  registerNetworkProxy() {
   1705    const enabled = Services.prefs.getBoolPref(PREF_IMAGE_PROXY_ENABLED, false);
   1706    if (!this._proxyRegistered && enabled) {
   1707      lazy.ProxyService.registerChannelFilter(this, 0);
   1708      this._proxyRegistered = true;
   1709    }
   1710  }
   1711 
   1712  /**
   1713   * Unregisters network proxy channel filter.
   1714   */
   1715  unregisterNetworkProxy() {
   1716    if (this._proxyRegistered) {
   1717      lazy.ProxyService.unregisterChannelFilter(this);
   1718      this._proxyRegistered = false;
   1719    }
   1720  }
   1721 
   1722  /**
   1723   * Retrieves and validates image proxy configuration from prefs/nimbus.
   1724   *
   1725   * @returns {object|null} Image proxy config object, or null if disabled/invalid.
   1726   */
   1727  getImageProxyConfig() {
   1728    try {
   1729      if (!this.store || !this.initialized) {
   1730        return null;
   1731      }
   1732 
   1733      const state = this.store.getState();
   1734      if (!state || !state.Prefs) {
   1735        return null;
   1736      }
   1737 
   1738      const { values } = state.Prefs;
   1739 
   1740      const config = values?.trainhopConfig?.imageProxy;
   1741      if (
   1742        !config ||
   1743        !config.enabled ||
   1744        !config.proxyHost ||
   1745        !config.proxyPort ||
   1746        !config.proxyAuthHeader ||
   1747        !values?.[PREF_INFERRED_ENABLED] ||
   1748        !values?.[PREF_IMAGE_PROXY_ENABLED_STORE]
   1749      ) {
   1750        return null;
   1751      }
   1752      return {
   1753        proxyHost: config.proxyHost,
   1754        proxyPort: config.proxyPort,
   1755        proxyAuthHeader: config.proxyAuthHeader,
   1756        connectionIsolationKey: config.connectionIsolationKey || "",
   1757        failoverProxy: config.failoverProxy,
   1758        imageProxyHosts: (config.imageProxyHosts || "")
   1759          .split(",")
   1760          .map(host => host.trim()),
   1761      };
   1762    } catch (e) {
   1763      return null;
   1764    }
   1765  }
   1766 
   1767  /**
   1768   * nsIProtocolProxyChannelFilter implementation. Applies MASQUE proxy
   1769   * to image requests from newtab when configured.
   1770   *
   1771   * @param {nsIChannel} channel
   1772   * @param {nsIProxyInfo} proxyInfo
   1773   * @param {nsIProtocolProxyChannelFilter} callback
   1774   */
   1775  applyFilter(channel, proxyInfo, callback) {
   1776    const { browsingContext } = channel.loadInfo;
   1777    let browser = browsingContext?.top?.embedderElement;
   1778 
   1779    if (!browser || !lazy.AboutNewTabParent.loadedTabs.has(browser)) {
   1780      callback.onProxyFilterResult(proxyInfo);
   1781      return;
   1782    }
   1783 
   1784    const config = this.getImageProxyConfig();
   1785 
   1786    if (!config) {
   1787      callback.onProxyFilterResult(proxyInfo);
   1788      return;
   1789    }
   1790 
   1791    if (
   1792      config.imageProxyHosts.includes(channel.URI.host) &&
   1793      channel.URI?.scheme === "https"
   1794    ) {
   1795      callback.onProxyFilterResult(
   1796        lazy.ProxyService.newProxyInfo(
   1797          "https" /* aType */,
   1798          config.proxyHost /* aHost */,
   1799          config.proxyPort /* aPort */,
   1800          config.proxyAuthHeader /* aProxyAuthorizationHeader */,
   1801          config.connectionIsolationKey /* aConnectionIsolationKey */,
   1802          0 /* aFlags */,
   1803          5000 /* aFailoverTimeout */,
   1804          config.failoverProxy /* aFailoverProxy */
   1805        )
   1806      );
   1807    } else {
   1808      callback.onProxyFilterResult(proxyInfo);
   1809    }
   1810  }
   1811 
   1812  QueryInterface = ChromeUtils.generateQI([Ci.nsIProtocolProxyChannelFilter]);
   1813 
   1814  /**
   1815   * Check if an old pref has a custom value to migrate. Clears the pref so that
   1816   * it's the default after migrating (to avoid future need to migrate).
   1817   *
   1818   * @param oldPrefName {string} Pref to check and migrate
   1819   * @param cbIfNotDefault {function} Callback that gets the current pref value
   1820   */
   1821  _migratePref(oldPrefName, cbIfNotDefault) {
   1822    // Nothing to do if the user doesn't have a custom value
   1823    if (!Services.prefs.prefHasUserValue(oldPrefName)) {
   1824      return;
   1825    }
   1826 
   1827    // Figure out what kind of pref getter to use
   1828    let prefGetter;
   1829    switch (Services.prefs.getPrefType(oldPrefName)) {
   1830      case Services.prefs.PREF_BOOL:
   1831        prefGetter = "getBoolPref";
   1832        break;
   1833      case Services.prefs.PREF_INT:
   1834        prefGetter = "getIntPref";
   1835        break;
   1836      case Services.prefs.PREF_STRING:
   1837        prefGetter = "getStringPref";
   1838        break;
   1839    }
   1840 
   1841    // Give the callback the current value then clear the pref
   1842    cbIfNotDefault(Services.prefs[prefGetter](oldPrefName));
   1843    Services.prefs.clearUserPref(oldPrefName);
   1844  }
   1845 
   1846  uninit() {
   1847    if (this.geo === "") {
   1848      Services.obs.removeObserver(this, lazy.Region.REGION_TOPIC);
   1849    }
   1850    delete this.geo;
   1851 
   1852    Services.obs.removeObserver(this, "intl:app-locales-changed");
   1853    Services.prefs.removeObserver(PREF_IMAGE_PROXY_ENABLED, this);
   1854 
   1855    this.store.uninit();
   1856    this.unregisterNetworkProxy();
   1857    this.initialized = false;
   1858  }
   1859 
   1860  _updateDynamicPrefs() {
   1861    // Save the geo pref if we have it
   1862    if (lazy.Region.home) {
   1863      if (this.geo === "") {
   1864        // The observer has become obsolete.
   1865        Services.obs.removeObserver(this, lazy.Region.REGION_TOPIC);
   1866      }
   1867      this.geo = lazy.Region.home;
   1868    } else if (this.geo !== "") {
   1869      // Watch for geo changes and use a dummy value for now
   1870      Services.obs.addObserver(this, lazy.Region.REGION_TOPIC);
   1871      this.geo = "";
   1872    }
   1873 
   1874    this.locale = Services.locale.appLocaleAsBCP47;
   1875 
   1876    // Update the pref config of those with dynamic values
   1877    for (const pref of PREFS_CONFIG.keys()) {
   1878      // Only need to process dynamic prefs
   1879      const prefConfig = PREFS_CONFIG.get(pref);
   1880      if (!prefConfig.getValue) {
   1881        continue;
   1882      }
   1883 
   1884      // Have the dynamic pref just reuse using existing default, e.g., those
   1885      // set via Autoconfig or policy
   1886      try {
   1887        const existingDefault = this._defaultPrefs.get(pref);
   1888        if (existingDefault !== undefined && prefConfig.value === undefined) {
   1889          prefConfig.getValue = () => existingDefault;
   1890        }
   1891      } catch (ex) {
   1892        // We get NS_ERROR_UNEXPECTED for prefs that have a user value (causing
   1893        // default branch to believe there's a type) but no actual default value
   1894      }
   1895 
   1896      // Compute the dynamic value (potentially generic based on dummy geo)
   1897      const newValue = prefConfig.getValue({
   1898        geo: this.geo,
   1899        locale: this.locale,
   1900      });
   1901 
   1902      // If there's an existing value and it has changed, that means we need to
   1903      // overwrite the default with the new value.
   1904      if (prefConfig.value !== undefined && prefConfig.value !== newValue) {
   1905        this._defaultPrefs.set(pref, newValue);
   1906      }
   1907 
   1908      prefConfig.value = newValue;
   1909    }
   1910  }
   1911 
   1912  observe(subject, topic, data) {
   1913    switch (topic) {
   1914      case "intl:app-locales-changed":
   1915      case lazy.Region.REGION_TOPIC:
   1916        this._updateDynamicPrefs();
   1917        break;
   1918      case "nsPref:changed":
   1919        if (data === PREF_IMAGE_PROXY_ENABLED) {
   1920          const enabled = Services.prefs.getBoolPref(
   1921            PREF_IMAGE_PROXY_ENABLED,
   1922            false
   1923          );
   1924          if (enabled) {
   1925            this.registerNetworkProxy();
   1926          } else {
   1927            this.unregisterNetworkProxy();
   1928          }
   1929        }
   1930        break;
   1931    }
   1932  }
   1933 }