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/. */
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).
11 // eslint-disable-next-line mozilla/use-static-import
12 const { AppConstants } = ChromeUtils.importESModule(
13 "resource://gre/modules/AppConstants.sys.mjs"
18 ChromeUtils.defineESModuleGetters(lazy, {
19 AboutPreferences: "resource://activity-stream/lib/AboutPreferences.sys.mjs",
20 DEFAULT_SITES: "resource://activity-stream/lib/DefaultSites.sys.mjs",
21 DefaultPrefs: "resource://activity-stream/lib/ActivityStreamPrefs.sys.mjs",
23 "resource://activity-stream/lib/DiscoveryStreamFeed.sys.mjs",
24 FaviconFeed: "resource://activity-stream/lib/FaviconFeed.sys.mjs",
25 HighlightsFeed: "resource://activity-stream/lib/HighlightsFeed.sys.mjs",
26 NewTabInit: "resource://activity-stream/lib/NewTabInit.sys.mjs",
27 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
28 PrefsFeed: "resource://activity-stream/lib/PrefsFeed.sys.mjs",
29 PlacesFeed: "resource://activity-stream/lib/PlacesFeed.sys.mjs",
30 RecommendationProvider:
31 "resource://activity-stream/lib/RecommendationProvider.sys.mjs",
32 Region: "resource://gre/modules/Region.sys.mjs",
33 SectionsFeed: "resource://activity-stream/lib/SectionsManager.sys.mjs",
34 Store: "resource://activity-stream/lib/Store.sys.mjs",
35 SystemTickFeed: "resource://activity-stream/lib/SystemTickFeed.sys.mjs",
36 TelemetryFeed: "resource://activity-stream/lib/TelemetryFeed.sys.mjs",
37 TopSitesFeed: "resource://activity-stream/lib/TopSitesFeed.sys.mjs",
38 TopStoriesFeed: "resource://activity-stream/lib/TopStoriesFeed.sys.mjs",
41 // NB: Eagerly load modules that will be loaded/constructed/initialized in the
42 // common case to avoid the overhead of wrapping and detecting lazy loading.
46 } from "resource://activity-stream/common/Actions.sys.mjs";
48 const REGION_BASIC_CONFIG =
49 "browser.newtabpage.activity-stream.discoverystream.region-basic-config";
51 // Determine if spocs should be shown for a geo/locale
52 function showSpocs({ geo }) {
53 const spocsGeoString =
54 lazy.NimbusFeatures.pocketNewtab.getVariable("regionSpocsConfig") || "";
55 const spocsGeo = spocsGeoString.split(",").map(s => s.trim());
56 return spocsGeo.includes(geo);
59 // Configure default Activity Stream prefs with a plain `value` or a `getValue`
60 // that computes a value. A `value_local_dev` is used for development defaults.
61 export const PREFS_CONFIG = new Map([
66 "Comma-separated list of default top sites to fill in behind visited sites",
67 getValue: ({ geo }) =>
68 lazy.DEFAULT_SITES.get(lazy.DEFAULT_SITES.has(geo) ? geo : ""),
72 "feeds.section.topstories.options",
74 title: "Configuration options for top stories feed",
75 // This is a dynamic pref as it depends on the feed being shown or not
78 api_key_pref: "extensions.pocket.oAuthConsumerKey",
79 // Use the opposite value as what default value the feed would have used
80 hidden: !PREFS_CONFIG.get("feeds.system.topstories").getValue(args),
81 provider_icon: "chrome://global/skin/icons/pocket.svg",
82 provider_name: "Pocket",
84 "https://getpocket.com/explore/trending?src=fx_new_tab",
85 stories_endpoint: `https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?version=3&consumer_key=$apiKey&locale_lang=${
88 showSpocs(args) ? "default_spocs_on" : "default_spocs_off"
90 stories_referrer: "https://getpocket.com/recommendations",
91 topics_endpoint: `https://getpocket.cdn.mozilla.net/v3/firefox/trending-topics?version=2&consumer_key=$apiKey&locale_lang=${args.locale}`,
92 show_spocs: showSpocs(args),
99 title: "Displays Top Sites on the New Tab Page",
107 "Hide the top sites section's title, including the section and collapse icons",
114 title: "User pref for sponsored Pocket content",
119 "system.showSponsored",
121 title: "System pref for sponsored Pocket content",
122 // This pref is dynamic as the sponsored content depends on the region
127 "showSponsoredTopSites",
129 title: "Show sponsored top sites",
136 title: "Pocket cta and button for logged out users.",
137 value: JSON.stringify({
148 title: "Show the Search bar",
155 title: "Number of rows of Top Sites to display",
162 title: "Enable system error and usage data collection",
164 value_local_dev: false,
168 "telemetry.ut.events",
170 title: "Enable Unified Telemetry event data collection",
171 value: AppConstants.EARLY_BETA_OR_EARLIER,
172 value_local_dev: false,
176 "telemetry.structuredIngestion.endpoint",
178 title: "Structured Ingestion telemetry server endpoint",
179 value: "https://incoming.telemetry.mozilla.org/submit",
183 "section.highlights.includeVisited",
186 "Boolean flag that decides whether or not to show visited pages in highlights.",
191 "section.highlights.includeBookmarks",
194 "Boolean flag that decides whether or not to show bookmarks in highlights.",
199 "section.highlights.includePocket",
202 "Boolean flag that decides whether or not to show saved Pocket stories in highlights.",
207 "section.highlights.includeDownloads",
210 "Boolean flag that decides whether or not to show saved recent Downloads in highlights.",
215 "section.highlights.rows",
217 title: "Number of rows of Highlights to display",
222 "section.topstories.rows",
224 title: "Number of rows of Top Stories to display",
231 title: "The rendering order for the sections",
232 value: "topsites,topstories,highlights",
236 "improvesearch.noDefaultSearchTile",
238 title: "Remove tiles that are the same as the default search",
243 "improvesearch.topSiteSearchShortcuts.searchEngines",
246 "An ordered, comma-delimited list of search shortcuts that we should try and pin",
247 // This pref is dynamic as the shortcuts vary depending on the region
248 getValue: ({ geo }) => {
252 const searchShortcuts = [];
254 searchShortcuts.push("baidu");
255 } else if (["BY", "KZ", "RU", "TR"].includes(geo)) {
256 searchShortcuts.push("yandex");
258 searchShortcuts.push("google");
260 if (["DE", "FR", "GB", "IT", "JP", "US"].includes(geo)) {
261 searchShortcuts.push("amazon");
263 return searchShortcuts.join(",");
268 "improvesearch.topSiteSearchShortcuts.havePinned",
271 "A comma-delimited list of search shortcuts that have previously been pinned",
276 "asrouter.devtoolsEnabled",
278 title: "Are the asrouter devtools enabled?",
283 "asrouter.providers.onboarding",
285 title: "Configuration for onboarding provider",
286 value: JSON.stringify({
289 localProvider: "OnboardingMessageProvider",
291 // Block specific messages from this local provider
296 // See browser/app/profile/firefox.js for other ASR preferences. They must be defined there to enable roll-outs.
298 "discoverystream.flight.blocks",
300 title: "Track flight blocks",
306 "discoverystream.config",
308 title: "Configuration for the new pocket new tab",
309 getValue: ({ geo, locale }) => {
310 return JSON.stringify({
311 api_key_pref: "extensions.pocket.oAuthConsumerKey",
319 "discoverystream.endpoints",
322 "Endpoint prefixes (comma-separated) that are allowed to be requested",
324 "https://getpocket.cdn.mozilla.net/,https://firefox-api-proxy.cdn.mozilla.net/,https://spocs.getpocket.com/",
328 "discoverystream.isCollectionDismissible",
330 title: "Allows Pocket story collections to be dismissed",
335 "discoverystream.onboardingExperience.dismissed",
337 title: "Allows the user to dismiss the new Pocket onboarding experience",
339 alsoToPreloaded: true,
344 "discoverystream.region-basic-layout",
346 title: "Decision to use basic layout based on region.",
347 getValue: ({ geo }) => {
348 const preffedRegionsString =
349 Services.prefs.getStringPref(REGION_BASIC_CONFIG) || "";
350 // If no regions are set to basic,
351 // we don't need to bother checking against the region.
352 // We are also not concerned if geo is not set,
353 // because stories are going to be empty until we have geo.
354 if (!preffedRegionsString) {
357 const preffedRegions = preffedRegionsString
361 return preffedRegions.includes(geo);
366 "discoverystream.spoc.impressions",
368 title: "Track spoc impressions",
374 "discoverystream.endpointSpocsClear",
377 "Endpoint for when a user opts-out of sponsored content to delete the user's data from the ad server.",
378 value: "https://spocs.getpocket.com/user",
382 "discoverystream.rec.impressions",
384 title: "Track rec impressions",
392 title: "Control whether a user wants recent saves visible on Newtab",
398 // Array of each feed's FEEDS_CONFIG factory and values to add to PREFS_CONFIG
401 name: "aboutpreferences",
402 factory: () => new lazy.AboutPreferences(),
403 title: "about:preferences rendering",
408 factory: () => new lazy.NewTabInit(),
409 title: "Sends a copy of the state to each new tab that is opened",
414 factory: () => new lazy.PlacesFeed(),
415 title: "Listens for and relays various Places-related events",
420 factory: () => new lazy.PrefsFeed(PREFS_CONFIG),
421 title: "Preferences",
426 factory: () => new lazy.SectionsFeed(),
427 title: "Manages sections",
431 name: "section.highlights",
432 factory: () => new lazy.HighlightsFeed(),
433 title: "Fetches content recommendations from places db",
437 name: "system.topstories",
439 new lazy.TopStoriesFeed(PREFS_CONFIG.get("discoverystream.config")),
441 "System pref that fetches content recommendations from a configurable content provider",
442 // Dynamically determine if Pocket should be shown for a geo / locale
443 getValue: ({ geo, locale }) => {
444 // If we don't have geo, we don't want to flash the screen with stories while geo loads.
445 // Best to display nothing until geo is ready.
449 const preffedRegionsBlockString =
450 lazy.NimbusFeatures.pocketNewtab.getVariable("regionStoriesBlock") ||
452 const preffedRegionsString =
453 lazy.NimbusFeatures.pocketNewtab.getVariable("regionStoriesConfig") ||
455 const preffedLocaleListString =
456 lazy.NimbusFeatures.pocketNewtab.getVariable("localeListConfig") || "";
457 const preffedBlockRegions = preffedRegionsBlockString
460 const preffedRegions = preffedRegionsString.split(",").map(s => s.trim());
461 const preffedLocales = preffedLocaleListString
465 US: ["en-CA", "en-GB", "en-US"],
466 CA: ["en-CA", "en-GB", "en-US"],
467 GB: ["en-CA", "en-GB", "en-US"],
468 AU: ["en-CA", "en-GB", "en-US"],
469 NZ: ["en-CA", "en-GB", "en-US"],
470 IN: ["en-CA", "en-GB", "en-US"],
471 IE: ["en-CA", "en-GB", "en-US"],
472 ZA: ["en-CA", "en-GB", "en-US"],
481 JP: ["ja", "ja-JP-mac"],
484 const regionBlocked = preffedBlockRegions.includes(geo);
485 const localeEnabled = locale && preffedLocales.includes(locale);
486 const regionEnabled =
487 preffedRegions.includes(geo) && !!locales && locales.includes(locale);
488 return !regionBlocked && (localeEnabled || regionEnabled);
493 factory: () => new lazy.SystemTickFeed(),
494 title: "Produces system tick events to periodically check for data expiry",
499 factory: () => new lazy.TelemetryFeed(),
500 title: "Relays telemetry-related actions to PingCentre",
505 factory: () => new lazy.FaviconFeed(),
506 title: "Fetches tippy top manifests from remote service",
510 name: "system.topsites",
511 factory: () => new lazy.TopSitesFeed(),
512 title: "Queries places and gets metadata for Top Sites section",
516 name: "recommendationprovider",
517 factory: () => new lazy.RecommendationProvider(),
518 title: "Handles setup and interaction for the personality provider",
522 name: "discoverystreamfeed",
523 factory: () => new lazy.DiscoveryStreamFeed(),
524 title: "Handles new pocket ui for the new tab page",
529 const FEEDS_CONFIG = new Map();
530 for (const config of FEEDS_DATA) {
531 const pref = `feeds.${config.name}`;
532 FEEDS_CONFIG.set(pref, config.factory);
533 PREFS_CONFIG.set(pref, config);
536 export class ActivityStream {
538 * constructor - Initializes an instance of ActivityStream
541 this.initialized = false;
542 this.store = new lazy.Store();
543 this.feeds = FEEDS_CONFIG;
544 this._defaultPrefs = new lazy.DefaultPrefs(PREFS_CONFIG);
549 this._updateDynamicPrefs();
550 this._defaultPrefs.init();
551 Services.obs.addObserver(this, "intl:app-locales-changed");
553 // Look for outdated user pref values that might have been accidentally
554 // persisted when restoring the original pref value at the end of an
555 // experiment across versions with a different default value.
557 "browser.newtabpage.activity-stream.discoverystream.config";
559 Services.prefs.prefHasUserValue(DS_CONFIG) &&
562 `{"api_key_pref":"extensions.pocket.oAuthConsumerKey","enabled":false,"show_spocs":true,"layout_endpoint":"https://getpocket.com/v3/newtab/layout?version=1&consumer_key=$apiKey&layout_variant=basic"}`,
564 `{"api_key_pref":"extensions.pocket.oAuthConsumerKey","enabled":false,"show_spocs":true,"layout_endpoint":"https://getpocket.cdn.mozilla.net/v3/newtab/layout?version=1&consumer_key=$apiKey&layout_variant=basic"}`,
566 `{"api_key_pref":"extensions.pocket.oAuthConsumerKey","collapsible":true,"enabled":false,"show_spocs":true,"hardcoded_layout":true,"personalized":false,"layout_endpoint":"https://getpocket.cdn.mozilla.net/v3/newtab/layout?version=1&consumer_key=$apiKey&layout_variant=basic"}`,
567 ].includes(Services.prefs.getStringPref(DS_CONFIG))
569 Services.prefs.clearUserPref(DS_CONFIG);
572 // Hook up the store and let all feeds and pages initialize
575 ac.BroadcastToContent({
587 this.initialized = true;
589 // TelemetryFeed could be unavailable if the telemetry is disabled, or
590 // the telemetry feed is not yet initialized.
591 const telemetryFeed = this.store.feeds.get("feeds.telemetry");
593 telemetryFeed.handleUndesiredEvent({
594 data: { event: "ADDON_INIT_FAILED" },
602 * Check if an old pref has a custom value to migrate. Clears the pref so that
603 * it's the default after migrating (to avoid future need to migrate).
605 * @param oldPrefName {string} Pref to check and migrate
606 * @param cbIfNotDefault {function} Callback that gets the current pref value
608 _migratePref(oldPrefName, cbIfNotDefault) {
609 // Nothing to do if the user doesn't have a custom value
610 if (!Services.prefs.prefHasUserValue(oldPrefName)) {
614 // Figure out what kind of pref getter to use
616 switch (Services.prefs.getPrefType(oldPrefName)) {
617 case Services.prefs.PREF_BOOL:
618 prefGetter = "getBoolPref";
620 case Services.prefs.PREF_INT:
621 prefGetter = "getIntPref";
623 case Services.prefs.PREF_STRING:
624 prefGetter = "getStringPref";
628 // Give the callback the current value then clear the pref
629 cbIfNotDefault(Services.prefs[prefGetter](oldPrefName));
630 Services.prefs.clearUserPref(oldPrefName);
634 if (this.geo === "") {
635 Services.obs.removeObserver(this, lazy.Region.REGION_TOPIC);
638 Services.obs.removeObserver(this, "intl:app-locales-changed");
641 this.initialized = false;
644 _updateDynamicPrefs() {
645 // Save the geo pref if we have it
646 if (lazy.Region.home) {
647 this.geo = lazy.Region.home;
648 } else if (this.geo !== "") {
649 // Watch for geo changes and use a dummy value for now
650 Services.obs.addObserver(this, lazy.Region.REGION_TOPIC);
654 this.locale = Services.locale.appLocaleAsBCP47;
656 // Update the pref config of those with dynamic values
657 for (const pref of PREFS_CONFIG.keys()) {
658 // Only need to process dynamic prefs
659 const prefConfig = PREFS_CONFIG.get(pref);
660 if (!prefConfig.getValue) {
664 // Have the dynamic pref just reuse using existing default, e.g., those
665 // set via Autoconfig or policy
667 const existingDefault = this._defaultPrefs.get(pref);
668 if (existingDefault !== undefined && prefConfig.value === undefined) {
669 prefConfig.getValue = () => existingDefault;
672 // We get NS_ERROR_UNEXPECTED for prefs that have a user value (causing
673 // default branch to believe there's a type) but no actual default value
676 // Compute the dynamic value (potentially generic based on dummy geo)
677 const newValue = prefConfig.getValue({
682 // If there's an existing value and it has changed, that means we need to
683 // overwrite the default with the new value.
684 if (prefConfig.value !== undefined && prefConfig.value !== newValue) {
685 this._defaultPrefs.set(pref, newValue);
688 prefConfig.value = newValue;
692 observe(subject, topic, data) {
694 case "intl:app-locales-changed":
695 case lazy.Region.REGION_TOPIC:
696 this._updateDynamicPrefs();