123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470 |
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- "use strict";
- const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
- ChromeUtils.defineModuleGetter(this, "AppConstants",
- "resource://gre/modules/AppConstants.jsm");
- ChromeUtils.defineModuleGetter(this, "UpdateUtils",
- "resource://gre/modules/UpdateUtils.jsm");
- // NB: Eagerly load modules that will be loaded/constructed/initialized in the
- // common case to avoid the overhead of wrapping and detecting lazy loading.
- const {actionCreators: ac, actionTypes: at} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm");
- const {AboutPreferences} = ChromeUtils.import("resource://activity-stream/lib/AboutPreferences.jsm");
- const {DefaultPrefs} = ChromeUtils.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm");
- const {NewTabInit} = ChromeUtils.import("resource://activity-stream/lib/NewTabInit.jsm");
- const {SectionsFeed} = ChromeUtils.import("resource://activity-stream/lib/SectionsManager.jsm");
- const {PlacesFeed} = ChromeUtils.import("resource://activity-stream/lib/PlacesFeed.jsm");
- const {PrefsFeed} = ChromeUtils.import("resource://activity-stream/lib/PrefsFeed.jsm");
- const {Store} = ChromeUtils.import("resource://activity-stream/lib/Store.jsm");
- const {SystemTickFeed} = ChromeUtils.import("resource://activity-stream/lib/SystemTickFeed.jsm");
- const {FaviconFeed} = ChromeUtils.import("resource://activity-stream/lib/FaviconFeed.jsm");
- const {TopSitesFeed} = ChromeUtils.import("resource://activity-stream/lib/TopSitesFeed.jsm");
- const {TopStoriesFeed} = ChromeUtils.import("resource://activity-stream/lib/TopStoriesFeed.jsm");
- const {HighlightsFeed} = ChromeUtils.import("resource://activity-stream/lib/HighlightsFeed.jsm");
- const {ASRouterFeed} = ChromeUtils.import("resource://activity-stream/lib/ASRouterFeed.jsm");
- const {DiscoveryStreamFeed} = ChromeUtils.import("resource://activity-stream/lib/DiscoveryStreamFeed.jsm");
- const DEFAULT_SITES = new Map([
- // This first item is the global list fallback for any unexpected geos
- ["", "https://www.parabola.nu/,https://www.gnu.org/,https://www.fsf.org/,https://libreplanet.org/"]
- ]);
- const GEO_PREF = "browser.search.region";
- const SPOCS_GEOS = ["US"];
- const IS_NIGHTLY_OR_UNBRANDED_BUILD = ["nightly", "default"].includes(UpdateUtils.getUpdateChannel(true));
- // Determine if spocs should be shown for a geo/locale
- function showSpocs({geo}) {
- return SPOCS_GEOS.includes(geo);
- }
- // Configure default Activity Stream prefs with a plain `value` or a `getValue`
- // that computes a value. A `value_local_dev` is used for development defaults.
- const PREFS_CONFIG = new Map([
- ["default.sites", {
- title: "Comma-separated list of default top sites to fill in behind visited sites",
- getValue: ({geo}) => DEFAULT_SITES.get(DEFAULT_SITES.has(geo) ? geo : ""),
- }],
- ["feeds.section.topstories.options", {
- title: "Configuration options for top stories feed",
- // This is a dynamic pref as it depends on the feed being shown or not
- getValue: args => JSON.stringify({
- api_key_pref: "extensions.pocket.oAuthConsumerKey",
- // Use the opposite value as what default value the feed would have used
- hidden: !PREFS_CONFIG.get("feeds.section.topstories").getValue(args),
- provider_icon: "pocket",
- provider_name: "Pocket",
- read_more_endpoint: "https://parabola.nu/feeds.section.topstories.options",
- stories_endpoint: `https://parabola.nu/feeds.section.topstories.options`,
- stories_referrer: "https://parabola.nu/feeds.section.topstories.options",
- topics_endpoint: `https://parabola.nu/feeds.section.topstories.options`,
- model_keys: ["nmf_model_animals", "nmf_model_business", "nmf_model_career", "nmf_model_datascience", "nmf_model_design", "nmf_model_education", "nmf_model_entertainment", "nmf_model_environment", "nmf_model_fashion", "nmf_model_finance", "nmf_model_food", "nmf_model_health", "nmf_model_home", "nmf_model_life", "nmf_model_marketing", "nmf_model_politics", "nmf_model_programming", "nmf_model_science", "nmf_model_shopping", "nmf_model_sports", "nmf_model_tech", "nmf_model_travel", "nb_model_animals", "nb_model_books", "nb_model_business", "nb_model_career", "nb_model_datascience", "nb_model_design", "nb_model_economics", "nb_model_education", "nb_model_entertainment", "nb_model_environment", "nb_model_fashion", "nb_model_finance", "nb_model_food", "nb_model_game", "nb_model_health", "nb_model_history", "nb_model_home", "nb_model_life", "nb_model_marketing", "nb_model_military", "nb_model_philosophy", "nb_model_photography", "nb_model_politics", "nb_model_productivity", "nb_model_programming", "nb_model_psychology", "nb_model_science", "nb_model_shopping", "nb_model_society", "nb_model_space", "nb_model_sports", "nb_model_tech", "nb_model_travel", "nb_model_writing"],
- show_spocs: showSpocs(args),
- personalized: true,
- version: 1,
- }),
- }],
- ["showSponsored", {
- title: "Show sponsored cards in spoc experiment (show_spocs in topstories.options has to be set to true as well)",
- value: false,
- }],
- ["pocketCta", {
- title: "Pocket cta and button for logged out users.",
- value: JSON.stringify({
- cta_button: "",
- cta_text: "",
- cta_url: "",
- use_cta: false,
- }),
- }],
- ["filterAdult", {
- title: "Remove adult pages from sites, highlights, etc.",
- value: true,
- }],
- ["prerender", {
- title: "Use the prerendered version of activity-stream.html. This is set automatically by PrefsFeed.jsm.",
- value: true,
- }],
- ["showSearch", {
- title: "Show the Search bar",
- value: true,
- }],
- ["feeds.snippets", {
- title: "Show snippets on activity stream",
- value: false,
- }],
- ["topSitesRows", {
- title: "Number of rows of Top Sites to display",
- value: 1,
- }],
- ["telemetry", {
- title: "Enable system error and usage data collection",
- value: false,
- value_local_dev: false,
- }],
- ["telemetry.ut.events", {
- title: "Enable Unified Telemetry event data collection",
- value: false,
- value_local_dev: false,
- }],
- ["telemetry.structuredIngestion", {
- title: "Enable Structured Ingestion Telemetry data collection",
- value: false,
- value_local_dev: false,
- }],
- ["telemetry.structuredIngestion.endpoint", {
- title: "Structured Ingestion telemetry server endpoint",
- value: "https://parabola.nu/telemetry.structuredIngestion.endpoint",
- }],
- ["telemetry.ping.endpoint", {
- title: "Telemetry server endpoint",
- value: "https://parabola.nu/telemetry.ping.endpoint",
- }],
- ["section.highlights.includeVisited", {
- title: "Boolean flag that decides whether or not to show visited pages in highlights.",
- value: true,
- }],
- ["section.highlights.includeBookmarks", {
- title: "Boolean flag that decides whether or not to show bookmarks in highlights.",
- value: true,
- }],
- ["section.highlights.includePocket", {
- title: "Boolean flag that decides whether or not to show saved Pocket stories in highlights.",
- value: true,
- }],
- ["section.highlights.includeDownloads", {
- title: "Boolean flag that decides whether or not to show saved recent Downloads in highlights.",
- value: true,
- }],
- ["section.highlights.rows", {
- title: "Number of rows of Highlights to display",
- value: 2,
- }],
- ["section.topstories.rows", {
- title: "Number of rows of Top Stories to display",
- value: 1,
- }],
- ["sectionOrder", {
- title: "The rendering order for the sections",
- value: "topsites,topstories,highlights",
- }],
- ["improvesearch.noDefaultSearchTile", {
- title: "Remove tiles that are the same as the default search",
- value: true,
- }],
- ["improvesearch.topSiteSearchShortcuts.searchEngines", {
- title: "An ordered, comma-delimited list of search shortcuts that we should try and pin",
- // This pref is dynamic as the shortcuts vary depending on the region
- getValue: ({geo}) => {
- if (!geo) {
- return "";
- }
- const searchShortcuts = [];
- if (geo === "CN") {
- searchShortcuts.push("baidu");
- } else if (["BY", "KZ", "RU", "TR"].includes(geo)) {
- searchShortcuts.push("yandex");
- } else {
- searchShortcuts.push("duckduckgo");
- }
- return searchShortcuts.join(",");
- },
- }],
- ["improvesearch.topSiteSearchShortcuts.havePinned", {
- title: "A comma-delimited list of search shortcuts that have previously been pinned",
- value: "",
- }],
- ["asrouter.devtoolsEnabled", {
- title: "Are the asrouter devtools enabled?",
- value: false,
- }],
- ["asrouter.userprefs.cfr.addons", {
- title: "Does the user allow CFR addon recommendations?",
- value: true,
- }],
- ["asrouter.userprefs.cfr.features", {
- title: "Does the user allow CFR feature recommendations?",
- value: true,
- }],
- ["asrouter.providers.onboarding", {
- title: "Configuration for onboarding provider",
- value: JSON.stringify({
- id: "onboarding",
- type: "local",
- localProvider: "OnboardingMessageProvider",
- enabled: true,
- // Block specific messages from this local provider
- exclude: [],
- }),
- }],
- ["asrouter.providers.cfr-fxa", {
- title: "Configuration for CFR FxA Messages provider",
- value: JSON.stringify({
- id: "cfr-fxa",
- enabled: true,
- type: "remote-settings",
- bucket: "cfr-fxa",
- frequency: {custom: [{period: "daily", cap: 1}]},
- }),
- }],
- // See browser/app/profile/firefox.js for other ASR preferences. They must be defined there to enable roll-outs.
- ["discoverystream.config", {
- title: "Configuration for the new pocket new tab",
- getValue: ({geo, locale}) => {
- const locales = ({
- "US": ["en-CA", "en-GB", "en-US", "en-ZA"],
- "CA": ["en-CA", "en-GB", "en-US", "en-ZA"],
- })[geo];
- const isEnabled = IS_NIGHTLY_OR_UNBRANDED_BUILD && locales && locales.includes(locale);
- return JSON.stringify({
- api_key_pref: "extensions.pocket.oAuthConsumerKey",
- collapsible: true,
- enabled: isEnabled,
- show_spocs: showSpocs({geo}),
- hardcoded_layout: true,
- personalized: false,
- // This is currently an exmple layout used for dev purposes.
- layout_endpoint: "https://parabola.nu/extensions.pocket.oAuthConsumerKey",
- });
- },
- }],
- ["discoverystream.endpoints", {
- title: "Endpoint prefixes (comma-separated) that are allowed to be requested",
- value: "https://parabola.nu/discoverystream.endpoints",
- }],
- ["discoverystream.spoc.impressions", {
- title: "Track spoc impressions",
- skipBroadcast: true,
- value: "{}",
- }],
- ["discoverystream.rec.impressions", {
- title: "Track rec impressions",
- skipBroadcast: true,
- value: "{}",
- }],
- ]);
- // Array of each feed's FEEDS_CONFIG factory and values to add to PREFS_CONFIG
- const FEEDS_DATA = [
- {
- name: "aboutpreferences",
- factory: () => new AboutPreferences(),
- title: "about:preferences rendering",
- value: true,
- },
- {
- name: "newtabinit",
- factory: () => new NewTabInit(),
- title: "Sends a copy of the state to each new tab that is opened",
- value: true,
- },
- {
- name: "places",
- factory: () => new PlacesFeed(),
- title: "Listens for and relays various Places-related events",
- value: true,
- },
- {
- name: "prefs",
- factory: () => new PrefsFeed(PREFS_CONFIG),
- title: "Preferences",
- value: true,
- },
- {
- name: "sections",
- factory: () => new SectionsFeed(),
- title: "Manages sections",
- value: true,
- },
- {
- name: "section.highlights",
- factory: () => new HighlightsFeed(),
- title: "Fetches content recommendations from places db",
- value: true,
- },
- {
- name: "section.topstories",
- factory: () => new TopStoriesFeed(PREFS_CONFIG.get("discoverystream.config")),
- title: "Fetches content recommendations from a configurable content provider",
- // Dynamically determine if Pocket should be shown for a geo / locale
- getValue: ({geo, locale}) => {
- const locales = ({
- "US": ["en-CA", "en-GB", "en-US", "en-ZA"],
- "CA": ["en-CA", "en-GB", "en-US", "en-ZA"],
- "DE": ["de", "de-DE", "de-AT", "de-CH"],
- })[geo];
- return false;
- },
- },
- {
- name: "systemtick",
- factory: () => new SystemTickFeed(),
- title: "Produces system tick events to periodically check for data expiry",
- value: true,
- },
- {
- name: "telemetry",
- factory: () => new TelemetryFeed(),
- title: "Relays telemetry-related actions to PingCentre",
- value: false,
- },
- {
- name: "favicon",
- factory: () => new FaviconFeed(),
- title: "Fetches tippy top manifests from remote service",
- value: true,
- },
- {
- name: "topsites",
- factory: () => new TopSitesFeed(),
- title: "Queries places and gets metadata for Top Sites section",
- value: true,
- },
- {
- name: "asrouterfeed",
- factory: () => new ASRouterFeed(),
- title: "Handles AS Router messages, such as snippets and onboaridng",
- value: true,
- },
- {
- name: "discoverystreamfeed",
- factory: () => new DiscoveryStreamFeed(),
- title: "Handles new pocket ui for the new tab page",
- value: true,
- },
- ];
- const FEEDS_CONFIG = new Map();
- for (const config of FEEDS_DATA) {
- const pref = `feeds.${config.name}`;
- FEEDS_CONFIG.set(pref, config.factory);
- PREFS_CONFIG.set(pref, config);
- }
- this.ActivityStream = class ActivityStream {
- /**
- * constructor - Initializes an instance of ActivityStream
- */
- constructor() {
- this.initialized = false;
- this.store = new Store();
- this.feeds = FEEDS_CONFIG;
- this._defaultPrefs = new DefaultPrefs(PREFS_CONFIG);
- }
- init() {
- try {
- this._updateDynamicPrefs();
- this._defaultPrefs.init();
- // Hook up the store and let all feeds and pages initialize
- this.store.init(this.feeds, ac.BroadcastToContent({
- type: at.INIT,
- data: {},
- }), {type: at.UNINIT});
- this.initialized = true;
- } catch (e) {
- // TelemetryFeed could be unavailable if the telemetry is disabled, or
- // the telemetry feed is not yet initialized.
- const telemetryFeed = this.store.feeds.get("feeds.telemetry");
- if (telemetryFeed) {
- telemetryFeed.handleUndesiredEvent({data: {event: "ADDON_INIT_FAILED"}});
- }
- throw e;
- }
- }
- /**
- * Check if an old pref has a custom value to migrate. Clears the pref so that
- * it's the default after migrating (to avoid future need to migrate).
- *
- * @param oldPrefName {string} Pref to check and migrate
- * @param cbIfNotDefault {function} Callback that gets the current pref value
- */
- _migratePref(oldPrefName, cbIfNotDefault) {
- // Nothing to do if the user doesn't have a custom value
- if (!Services.prefs.prefHasUserValue(oldPrefName)) {
- return;
- }
- // Figure out what kind of pref getter to use
- let prefGetter;
- switch (Services.prefs.getPrefType(oldPrefName)) {
- case Services.prefs.PREF_BOOL:
- prefGetter = "getBoolPref";
- break;
- case Services.prefs.PREF_INT:
- prefGetter = "getIntPref";
- break;
- case Services.prefs.PREF_STRING:
- prefGetter = "getStringPref";
- break;
- }
- // Give the callback the current value then clear the pref
- cbIfNotDefault(Services.prefs[prefGetter](oldPrefName));
- Services.prefs.clearUserPref(oldPrefName);
- }
- uninit() {
- if (this.geo === "") {
- Services.prefs.removeObserver(GEO_PREF, this);
- }
- this.store.uninit();
- this.initialized = false;
- }
- _updateDynamicPrefs() {
- // Save the geo pref if we have it
- if (Services.prefs.prefHasUserValue(GEO_PREF)) {
- this.geo = Services.prefs.getStringPref(GEO_PREF);
- } else if (this.geo !== "") {
- // Watch for geo changes and use a dummy value for now
- Services.prefs.addObserver(GEO_PREF, this);
- this.geo = "";
- }
- this.locale = Services.locale.appLocaleAsLangTag;
- // Update the pref config of those with dynamic values
- for (const pref of PREFS_CONFIG.keys()) {
- const prefConfig = PREFS_CONFIG.get(pref);
- if (!prefConfig.getValue) {
- continue;
- }
- const newValue = prefConfig.getValue({
- geo: this.geo,
- locale: this.locale,
- });
- // If there's an existing value and it has changed, that means we need to
- // overwrite the default with the new value.
- if (prefConfig.value !== undefined && prefConfig.value !== newValue) {
- this._defaultPrefs.set(pref, newValue);
- }
- prefConfig.value = newValue;
- }
- }
- observe(subject, topic, data) {
- switch (topic) {
- case "nsPref:changed":
- // We should only expect one geo change, so update and stop observing
- if (data === GEO_PREF) {
- this._updateDynamicPrefs();
- Services.prefs.removeObserver(GEO_PREF, this);
- }
- break;
- }
- }
- };
- const EXPORTED_SYMBOLS = ["ActivityStream", "PREFS_CONFIG"];
|