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",
39 WallpaperFeed: "resource://activity-stream/lib/WallpaperFeed.sys.mjs",
42 // NB: Eagerly load modules that will be loaded/constructed/initialized in the
43 // common case to avoid the overhead of wrapping and detecting lazy loading.
47 } from "resource://activity-stream/common/Actions.mjs";
49 const REGION_BASIC_CONFIG =
50 "browser.newtabpage.activity-stream.discoverystream.region-basic-config";
52 // Determine if spocs should be shown for a geo/locale
53 function showSpocs({ geo }) {
54 const spocsGeoString =
55 lazy.NimbusFeatures.pocketNewtab.getVariable("regionSpocsConfig") || "";
56 const spocsGeo = spocsGeoString.split(",").map(s => s.trim());
57 return spocsGeo.includes(geo);
60 // Configure default Activity Stream prefs with a plain `value` or a `getValue`
61 // that computes a value. A `value_local_dev` is used for development defaults.
62 export const PREFS_CONFIG = new Map([
67 "Comma-separated list of default top sites to fill in behind visited sites",
68 getValue: ({ geo }) =>
69 lazy.DEFAULT_SITES.get(lazy.DEFAULT_SITES.has(geo) ? geo : ""),
73 "feeds.section.topstories.options",
75 title: "Configuration options for top stories feed",
76 // This is a dynamic pref as it depends on the feed being shown or not
79 api_key_pref: "extensions.pocket.oAuthConsumerKey",
80 // Use the opposite value as what default value the feed would have used
81 hidden: !PREFS_CONFIG.get("feeds.system.topstories").getValue(args),
82 provider_icon: "chrome://global/skin/icons/pocket.svg",
83 provider_name: "Pocket",
85 "https://getpocket.com/explore/trending?src=fx_new_tab",
86 stories_endpoint: `https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?version=3&consumer_key=$apiKey&locale_lang=${
89 showSpocs(args) ? "default_spocs_on" : "default_spocs_off"
91 stories_referrer: "https://getpocket.com/recommendations",
92 topics_endpoint: `https://getpocket.cdn.mozilla.net/v3/firefox/trending-topics?version=2&consumer_key=$apiKey&locale_lang=${args.locale}`,
93 show_spocs: showSpocs(args),
100 title: "Displays Top Sites on the New Tab Page",
108 "Hide the top sites section's title, including the section and collapse icons",
115 title: "User pref for sponsored Pocket content",
120 "system.showSponsored",
122 title: "System pref for sponsored Pocket content",
123 // This pref is dynamic as the sponsored content depends on the region
128 "showSponsoredTopSites",
130 title: "Show sponsored top sites",
137 title: "Pocket cta and button for logged out users.",
138 value: JSON.stringify({
149 title: "Show the Search bar",
156 title: "Number of rows of Top Sites to display",
163 title: "Enable system error and usage data collection",
165 value_local_dev: false,
169 "telemetry.ut.events",
171 title: "Enable Unified Telemetry event data collection",
172 value: AppConstants.EARLY_BETA_OR_EARLIER,
173 value_local_dev: false,
177 "telemetry.structuredIngestion.endpoint",
179 title: "Structured Ingestion telemetry server endpoint",
180 value: "https://incoming.telemetry.mozilla.org/submit",
184 "section.highlights.includeVisited",
187 "Boolean flag that decides whether or not to show visited pages in highlights.",
192 "section.highlights.includeBookmarks",
195 "Boolean flag that decides whether or not to show bookmarks in highlights.",
200 "section.highlights.includePocket",
203 "Boolean flag that decides whether or not to show saved Pocket stories in highlights.",
208 "section.highlights.includeDownloads",
211 "Boolean flag that decides whether or not to show saved recent Downloads in highlights.",
216 "section.highlights.rows",
218 title: "Number of rows of Highlights to display",
223 "section.topstories.rows",
225 title: "Number of rows of Top Stories to display",
232 title: "The rendering order for the sections",
233 value: "topsites,topstories,highlights",
237 "newtabWallpapers.enabled",
239 title: "Boolean flag to turn wallpaper functionality on and off",
244 "newtabWallpapers.wallpaper",
246 title: "Currently set wallpaper",
251 "improvesearch.noDefaultSearchTile",
253 title: "Remove tiles that are the same as the default search",
258 "improvesearch.topSiteSearchShortcuts.searchEngines",
261 "An ordered, comma-delimited list of search shortcuts that we should try and pin",
262 // This pref is dynamic as the shortcuts vary depending on the region
263 getValue: ({ geo }) => {
267 const searchShortcuts = [];
269 searchShortcuts.push("baidu");
270 } else if (["BY", "KZ", "RU", "TR"].includes(geo)) {
271 searchShortcuts.push("yandex");
273 searchShortcuts.push("google");
275 if (["DE", "FR", "GB", "IT", "JP", "US"].includes(geo)) {
276 searchShortcuts.push("amazon");
278 return searchShortcuts.join(",");
283 "improvesearch.topSiteSearchShortcuts.havePinned",
286 "A comma-delimited list of search shortcuts that have previously been pinned",
291 "asrouter.devtoolsEnabled",
293 title: "Are the asrouter devtools enabled?",
298 "asrouter.providers.onboarding",
300 title: "Configuration for onboarding provider",
301 value: JSON.stringify({
304 localProvider: "OnboardingMessageProvider",
306 // Block specific messages from this local provider
311 // See browser/app/profile/firefox.js for other ASR preferences. They must be defined there to enable roll-outs.
313 "discoverystream.flight.blocks",
315 title: "Track flight blocks",
321 "discoverystream.config",
323 title: "Configuration for the new pocket new tab",
325 return JSON.stringify({
326 api_key_pref: "extensions.pocket.oAuthConsumerKey",
334 "discoverystream.endpoints",
337 "Endpoint prefixes (comma-separated) that are allowed to be requested",
339 "https://getpocket.cdn.mozilla.net/,https://firefox-api-proxy.cdn.mozilla.net/,https://spocs.getpocket.com/",
343 "discoverystream.isCollectionDismissible",
345 title: "Allows Pocket story collections to be dismissed",
350 "discoverystream.onboardingExperience.dismissed",
352 title: "Allows the user to dismiss the new Pocket onboarding experience",
354 alsoToPreloaded: true,
359 "discoverystream.region-basic-layout",
361 title: "Decision to use basic layout based on region.",
362 getValue: ({ geo }) => {
363 const preffedRegionsString =
364 Services.prefs.getStringPref(REGION_BASIC_CONFIG) || "";
365 // If no regions are set to basic,
366 // we don't need to bother checking against the region.
367 // We are also not concerned if geo is not set,
368 // because stories are going to be empty until we have geo.
369 if (!preffedRegionsString) {
372 const preffedRegions = preffedRegionsString
376 return preffedRegions.includes(geo);
381 "discoverystream.spoc.impressions",
383 title: "Track spoc impressions",
389 "discoverystream.endpointSpocsClear",
392 "Endpoint for when a user opts-out of sponsored content to delete the user's data from the ad server.",
393 value: "https://spocs.getpocket.com/user",
397 "discoverystream.rec.impressions",
399 title: "Track rec impressions",
407 title: "Control whether a user wants recent saves visible on Newtab",
413 // Array of each feed's FEEDS_CONFIG factory and values to add to PREFS_CONFIG
416 name: "aboutpreferences",
417 factory: () => new lazy.AboutPreferences(),
418 title: "about:preferences rendering",
423 factory: () => new lazy.NewTabInit(),
424 title: "Sends a copy of the state to each new tab that is opened",
429 factory: () => new lazy.PlacesFeed(),
430 title: "Listens for and relays various Places-related events",
435 factory: () => new lazy.PrefsFeed(PREFS_CONFIG),
436 title: "Preferences",
441 factory: () => new lazy.SectionsFeed(),
442 title: "Manages sections",
446 name: "section.highlights",
447 factory: () => new lazy.HighlightsFeed(),
448 title: "Fetches content recommendations from places db",
452 name: "system.topstories",
454 new lazy.TopStoriesFeed(PREFS_CONFIG.get("discoverystream.config")),
456 "System pref that fetches content recommendations from a configurable content provider",
457 // Dynamically determine if Pocket should be shown for a geo / locale
458 getValue: ({ geo, locale }) => {
459 // If we don't have geo, we don't want to flash the screen with stories while geo loads.
460 // Best to display nothing until geo is ready.
464 const preffedRegionsBlockString =
465 lazy.NimbusFeatures.pocketNewtab.getVariable("regionStoriesBlock") ||
467 const preffedRegionsString =
468 lazy.NimbusFeatures.pocketNewtab.getVariable("regionStoriesConfig") ||
470 const preffedLocaleListString =
471 lazy.NimbusFeatures.pocketNewtab.getVariable("localeListConfig") || "";
472 const preffedBlockRegions = preffedRegionsBlockString
475 const preffedRegions = preffedRegionsString.split(",").map(s => s.trim());
476 const preffedLocales = preffedLocaleListString
480 US: ["en-CA", "en-GB", "en-US"],
481 CA: ["en-CA", "en-GB", "en-US"],
482 GB: ["en-CA", "en-GB", "en-US"],
483 AU: ["en-CA", "en-GB", "en-US"],
484 NZ: ["en-CA", "en-GB", "en-US"],
485 IN: ["en-CA", "en-GB", "en-US"],
486 IE: ["en-CA", "en-GB", "en-US"],
487 ZA: ["en-CA", "en-GB", "en-US"],
496 JP: ["ja", "ja-JP-mac"],
499 const regionBlocked = preffedBlockRegions.includes(geo);
500 const localeEnabled = locale && preffedLocales.includes(locale);
501 const regionEnabled =
502 preffedRegions.includes(geo) && !!locales && locales.includes(locale);
503 return !regionBlocked && (localeEnabled || regionEnabled);
508 factory: () => new lazy.SystemTickFeed(),
509 title: "Produces system tick events to periodically check for data expiry",
514 factory: () => new lazy.TelemetryFeed(),
515 title: "Relays telemetry-related actions to PingCentre",
520 factory: () => new lazy.FaviconFeed(),
521 title: "Fetches tippy top manifests from remote service",
525 name: "system.topsites",
526 factory: () => new lazy.TopSitesFeed(),
527 title: "Queries places and gets metadata for Top Sites section",
531 name: "recommendationprovider",
532 factory: () => new lazy.RecommendationProvider(),
533 title: "Handles setup and interaction for the personality provider",
537 name: "discoverystreamfeed",
538 factory: () => new lazy.DiscoveryStreamFeed(),
539 title: "Handles new pocket ui for the new tab page",
543 name: "wallpaperfeed",
544 factory: () => new lazy.WallpaperFeed(),
545 title: "Handles fetching and managing wallpaper data from RemoteSettings",
550 const FEEDS_CONFIG = new Map();
551 for (const config of FEEDS_DATA) {
552 const pref = `feeds.${config.name}`;
553 FEEDS_CONFIG.set(pref, config.factory);
554 PREFS_CONFIG.set(pref, config);
557 export class ActivityStream {
559 * constructor - Initializes an instance of ActivityStream
562 this.initialized = false;
563 this.store = new lazy.Store();
564 this.feeds = FEEDS_CONFIG;
565 this._defaultPrefs = new lazy.DefaultPrefs(PREFS_CONFIG);
570 this._updateDynamicPrefs();
571 this._defaultPrefs.init();
572 Services.obs.addObserver(this, "intl:app-locales-changed");
574 // Look for outdated user pref values that might have been accidentally
575 // persisted when restoring the original pref value at the end of an
576 // experiment across versions with a different default value.
578 "browser.newtabpage.activity-stream.discoverystream.config";
580 Services.prefs.prefHasUserValue(DS_CONFIG) &&
583 `{"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"}`,
585 `{"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"}`,
587 `{"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"}`,
588 ].includes(Services.prefs.getStringPref(DS_CONFIG))
590 Services.prefs.clearUserPref(DS_CONFIG);
593 // Hook up the store and let all feeds and pages initialize
596 ac.BroadcastToContent({
608 this.initialized = true;
610 // TelemetryFeed could be unavailable if the telemetry is disabled, or
611 // the telemetry feed is not yet initialized.
612 const telemetryFeed = this.store.feeds.get("feeds.telemetry");
614 telemetryFeed.handleUndesiredEvent({
615 data: { event: "ADDON_INIT_FAILED" },
623 * Check if an old pref has a custom value to migrate. Clears the pref so that
624 * it's the default after migrating (to avoid future need to migrate).
626 * @param oldPrefName {string} Pref to check and migrate
627 * @param cbIfNotDefault {function} Callback that gets the current pref value
629 _migratePref(oldPrefName, cbIfNotDefault) {
630 // Nothing to do if the user doesn't have a custom value
631 if (!Services.prefs.prefHasUserValue(oldPrefName)) {
635 // Figure out what kind of pref getter to use
637 switch (Services.prefs.getPrefType(oldPrefName)) {
638 case Services.prefs.PREF_BOOL:
639 prefGetter = "getBoolPref";
641 case Services.prefs.PREF_INT:
642 prefGetter = "getIntPref";
644 case Services.prefs.PREF_STRING:
645 prefGetter = "getStringPref";
649 // Give the callback the current value then clear the pref
650 cbIfNotDefault(Services.prefs[prefGetter](oldPrefName));
651 Services.prefs.clearUserPref(oldPrefName);
655 if (this.geo === "") {
656 Services.obs.removeObserver(this, lazy.Region.REGION_TOPIC);
659 Services.obs.removeObserver(this, "intl:app-locales-changed");
662 this.initialized = false;
665 _updateDynamicPrefs() {
666 // Save the geo pref if we have it
667 if (lazy.Region.home) {
668 this.geo = lazy.Region.home;
669 } else if (this.geo !== "") {
670 // Watch for geo changes and use a dummy value for now
671 Services.obs.addObserver(this, lazy.Region.REGION_TOPIC);
675 this.locale = Services.locale.appLocaleAsBCP47;
677 // Update the pref config of those with dynamic values
678 for (const pref of PREFS_CONFIG.keys()) {
679 // Only need to process dynamic prefs
680 const prefConfig = PREFS_CONFIG.get(pref);
681 if (!prefConfig.getValue) {
685 // Have the dynamic pref just reuse using existing default, e.g., those
686 // set via Autoconfig or policy
688 const existingDefault = this._defaultPrefs.get(pref);
689 if (existingDefault !== undefined && prefConfig.value === undefined) {
690 prefConfig.getValue = () => existingDefault;
693 // We get NS_ERROR_UNEXPECTED for prefs that have a user value (causing
694 // default branch to believe there's a type) but no actual default value
697 // Compute the dynamic value (potentially generic based on dummy geo)
698 const newValue = prefConfig.getValue({
703 // If there's an existing value and it has changed, that means we need to
704 // overwrite the default with the new value.
705 if (prefConfig.value !== undefined && prefConfig.value !== newValue) {
706 this._defaultPrefs.set(pref, newValue);
709 prefConfig.value = newValue;
713 observe(subject, topic) {
715 case "intl:app-locales-changed":
716 case lazy.Region.REGION_TOPIC:
717 this._updateDynamicPrefs();