ActivityStream.jsm 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  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. "use strict";
  5. const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
  6. ChromeUtils.defineModuleGetter(this, "AppConstants",
  7. "resource://gre/modules/AppConstants.jsm");
  8. ChromeUtils.defineModuleGetter(this, "UpdateUtils",
  9. "resource://gre/modules/UpdateUtils.jsm");
  10. // NB: Eagerly load modules that will be loaded/constructed/initialized in the
  11. // common case to avoid the overhead of wrapping and detecting lazy loading.
  12. const {actionCreators: ac, actionTypes: at} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm");
  13. const {AboutPreferences} = ChromeUtils.import("resource://activity-stream/lib/AboutPreferences.jsm");
  14. const {DefaultPrefs} = ChromeUtils.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm");
  15. const {NewTabInit} = ChromeUtils.import("resource://activity-stream/lib/NewTabInit.jsm");
  16. const {SectionsFeed} = ChromeUtils.import("resource://activity-stream/lib/SectionsManager.jsm");
  17. const {PlacesFeed} = ChromeUtils.import("resource://activity-stream/lib/PlacesFeed.jsm");
  18. const {PrefsFeed} = ChromeUtils.import("resource://activity-stream/lib/PrefsFeed.jsm");
  19. const {Store} = ChromeUtils.import("resource://activity-stream/lib/Store.jsm");
  20. const {SystemTickFeed} = ChromeUtils.import("resource://activity-stream/lib/SystemTickFeed.jsm");
  21. const {FaviconFeed} = ChromeUtils.import("resource://activity-stream/lib/FaviconFeed.jsm");
  22. const {TopSitesFeed} = ChromeUtils.import("resource://activity-stream/lib/TopSitesFeed.jsm");
  23. const {TopStoriesFeed} = ChromeUtils.import("resource://activity-stream/lib/TopStoriesFeed.jsm");
  24. const {HighlightsFeed} = ChromeUtils.import("resource://activity-stream/lib/HighlightsFeed.jsm");
  25. const {ASRouterFeed} = ChromeUtils.import("resource://activity-stream/lib/ASRouterFeed.jsm");
  26. const {DiscoveryStreamFeed} = ChromeUtils.import("resource://activity-stream/lib/DiscoveryStreamFeed.jsm");
  27. const DEFAULT_SITES = new Map([
  28. // This first item is the global list fallback for any unexpected geos
  29. ["", "https://www.parabola.nu/,https://www.gnu.org/,https://www.fsf.org/,https://libreplanet.org/"]
  30. ]);
  31. const GEO_PREF = "browser.search.region";
  32. const SPOCS_GEOS = ["US"];
  33. const IS_NIGHTLY_OR_UNBRANDED_BUILD = ["nightly", "default"].includes(UpdateUtils.getUpdateChannel(true));
  34. // Determine if spocs should be shown for a geo/locale
  35. function showSpocs({geo}) {
  36. return SPOCS_GEOS.includes(geo);
  37. }
  38. // Configure default Activity Stream prefs with a plain `value` or a `getValue`
  39. // that computes a value. A `value_local_dev` is used for development defaults.
  40. const PREFS_CONFIG = new Map([
  41. ["default.sites", {
  42. title: "Comma-separated list of default top sites to fill in behind visited sites",
  43. getValue: ({geo}) => DEFAULT_SITES.get(DEFAULT_SITES.has(geo) ? geo : ""),
  44. }],
  45. ["feeds.section.topstories.options", {
  46. title: "Configuration options for top stories feed",
  47. // This is a dynamic pref as it depends on the feed being shown or not
  48. getValue: args => JSON.stringify({
  49. api_key_pref: "extensions.pocket.oAuthConsumerKey",
  50. // Use the opposite value as what default value the feed would have used
  51. hidden: !PREFS_CONFIG.get("feeds.section.topstories").getValue(args),
  52. provider_icon: "pocket",
  53. provider_name: "Pocket",
  54. read_more_endpoint: "https://parabola.nu/feeds.section.topstories.options",
  55. stories_endpoint: `https://parabola.nu/feeds.section.topstories.options`,
  56. stories_referrer: "https://parabola.nu/feeds.section.topstories.options",
  57. topics_endpoint: `https://parabola.nu/feeds.section.topstories.options`,
  58. 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"],
  59. show_spocs: showSpocs(args),
  60. personalized: true,
  61. version: 1,
  62. }),
  63. }],
  64. ["showSponsored", {
  65. title: "Show sponsored cards in spoc experiment (show_spocs in topstories.options has to be set to true as well)",
  66. value: false,
  67. }],
  68. ["pocketCta", {
  69. title: "Pocket cta and button for logged out users.",
  70. value: JSON.stringify({
  71. cta_button: "",
  72. cta_text: "",
  73. cta_url: "",
  74. use_cta: false,
  75. }),
  76. }],
  77. ["filterAdult", {
  78. title: "Remove adult pages from sites, highlights, etc.",
  79. value: true,
  80. }],
  81. ["prerender", {
  82. title: "Use the prerendered version of activity-stream.html. This is set automatically by PrefsFeed.jsm.",
  83. value: true,
  84. }],
  85. ["showSearch", {
  86. title: "Show the Search bar",
  87. value: true,
  88. }],
  89. ["feeds.snippets", {
  90. title: "Show snippets on activity stream",
  91. value: false,
  92. }],
  93. ["topSitesRows", {
  94. title: "Number of rows of Top Sites to display",
  95. value: 1,
  96. }],
  97. ["telemetry", {
  98. title: "Enable system error and usage data collection",
  99. value: false,
  100. value_local_dev: false,
  101. }],
  102. ["telemetry.ut.events", {
  103. title: "Enable Unified Telemetry event data collection",
  104. value: false,
  105. value_local_dev: false,
  106. }],
  107. ["telemetry.structuredIngestion", {
  108. title: "Enable Structured Ingestion Telemetry data collection",
  109. value: false,
  110. value_local_dev: false,
  111. }],
  112. ["telemetry.structuredIngestion.endpoint", {
  113. title: "Structured Ingestion telemetry server endpoint",
  114. value: "https://parabola.nu/telemetry.structuredIngestion.endpoint",
  115. }],
  116. ["telemetry.ping.endpoint", {
  117. title: "Telemetry server endpoint",
  118. value: "https://parabola.nu/telemetry.ping.endpoint",
  119. }],
  120. ["section.highlights.includeVisited", {
  121. title: "Boolean flag that decides whether or not to show visited pages in highlights.",
  122. value: true,
  123. }],
  124. ["section.highlights.includeBookmarks", {
  125. title: "Boolean flag that decides whether or not to show bookmarks in highlights.",
  126. value: true,
  127. }],
  128. ["section.highlights.includePocket", {
  129. title: "Boolean flag that decides whether or not to show saved Pocket stories in highlights.",
  130. value: true,
  131. }],
  132. ["section.highlights.includeDownloads", {
  133. title: "Boolean flag that decides whether or not to show saved recent Downloads in highlights.",
  134. value: true,
  135. }],
  136. ["section.highlights.rows", {
  137. title: "Number of rows of Highlights to display",
  138. value: 2,
  139. }],
  140. ["section.topstories.rows", {
  141. title: "Number of rows of Top Stories to display",
  142. value: 1,
  143. }],
  144. ["sectionOrder", {
  145. title: "The rendering order for the sections",
  146. value: "topsites,topstories,highlights",
  147. }],
  148. ["improvesearch.noDefaultSearchTile", {
  149. title: "Remove tiles that are the same as the default search",
  150. value: true,
  151. }],
  152. ["improvesearch.topSiteSearchShortcuts.searchEngines", {
  153. title: "An ordered, comma-delimited list of search shortcuts that we should try and pin",
  154. // This pref is dynamic as the shortcuts vary depending on the region
  155. getValue: ({geo}) => {
  156. if (!geo) {
  157. return "";
  158. }
  159. const searchShortcuts = [];
  160. if (geo === "CN") {
  161. searchShortcuts.push("baidu");
  162. } else if (["BY", "KZ", "RU", "TR"].includes(geo)) {
  163. searchShortcuts.push("yandex");
  164. } else {
  165. searchShortcuts.push("duckduckgo");
  166. }
  167. return searchShortcuts.join(",");
  168. },
  169. }],
  170. ["improvesearch.topSiteSearchShortcuts.havePinned", {
  171. title: "A comma-delimited list of search shortcuts that have previously been pinned",
  172. value: "",
  173. }],
  174. ["asrouter.devtoolsEnabled", {
  175. title: "Are the asrouter devtools enabled?",
  176. value: false,
  177. }],
  178. ["asrouter.userprefs.cfr.addons", {
  179. title: "Does the user allow CFR addon recommendations?",
  180. value: true,
  181. }],
  182. ["asrouter.userprefs.cfr.features", {
  183. title: "Does the user allow CFR feature recommendations?",
  184. value: true,
  185. }],
  186. ["asrouter.providers.onboarding", {
  187. title: "Configuration for onboarding provider",
  188. value: JSON.stringify({
  189. id: "onboarding",
  190. type: "local",
  191. localProvider: "OnboardingMessageProvider",
  192. enabled: true,
  193. // Block specific messages from this local provider
  194. exclude: [],
  195. }),
  196. }],
  197. ["asrouter.providers.cfr-fxa", {
  198. title: "Configuration for CFR FxA Messages provider",
  199. value: JSON.stringify({
  200. id: "cfr-fxa",
  201. enabled: true,
  202. type: "remote-settings",
  203. bucket: "cfr-fxa",
  204. frequency: {custom: [{period: "daily", cap: 1}]},
  205. }),
  206. }],
  207. // See browser/app/profile/firefox.js for other ASR preferences. They must be defined there to enable roll-outs.
  208. ["discoverystream.config", {
  209. title: "Configuration for the new pocket new tab",
  210. getValue: ({geo, locale}) => {
  211. const locales = ({
  212. "US": ["en-CA", "en-GB", "en-US", "en-ZA"],
  213. "CA": ["en-CA", "en-GB", "en-US", "en-ZA"],
  214. })[geo];
  215. const isEnabled = IS_NIGHTLY_OR_UNBRANDED_BUILD && locales && locales.includes(locale);
  216. return JSON.stringify({
  217. api_key_pref: "extensions.pocket.oAuthConsumerKey",
  218. collapsible: true,
  219. enabled: isEnabled,
  220. show_spocs: showSpocs({geo}),
  221. hardcoded_layout: true,
  222. personalized: false,
  223. // This is currently an exmple layout used for dev purposes.
  224. layout_endpoint: "https://parabola.nu/extensions.pocket.oAuthConsumerKey",
  225. });
  226. },
  227. }],
  228. ["discoverystream.endpoints", {
  229. title: "Endpoint prefixes (comma-separated) that are allowed to be requested",
  230. value: "https://parabola.nu/discoverystream.endpoints",
  231. }],
  232. ["discoverystream.spoc.impressions", {
  233. title: "Track spoc impressions",
  234. skipBroadcast: true,
  235. value: "{}",
  236. }],
  237. ["discoverystream.rec.impressions", {
  238. title: "Track rec impressions",
  239. skipBroadcast: true,
  240. value: "{}",
  241. }],
  242. ]);
  243. // Array of each feed's FEEDS_CONFIG factory and values to add to PREFS_CONFIG
  244. const FEEDS_DATA = [
  245. {
  246. name: "aboutpreferences",
  247. factory: () => new AboutPreferences(),
  248. title: "about:preferences rendering",
  249. value: true,
  250. },
  251. {
  252. name: "newtabinit",
  253. factory: () => new NewTabInit(),
  254. title: "Sends a copy of the state to each new tab that is opened",
  255. value: true,
  256. },
  257. {
  258. name: "places",
  259. factory: () => new PlacesFeed(),
  260. title: "Listens for and relays various Places-related events",
  261. value: true,
  262. },
  263. {
  264. name: "prefs",
  265. factory: () => new PrefsFeed(PREFS_CONFIG),
  266. title: "Preferences",
  267. value: true,
  268. },
  269. {
  270. name: "sections",
  271. factory: () => new SectionsFeed(),
  272. title: "Manages sections",
  273. value: true,
  274. },
  275. {
  276. name: "section.highlights",
  277. factory: () => new HighlightsFeed(),
  278. title: "Fetches content recommendations from places db",
  279. value: true,
  280. },
  281. {
  282. name: "section.topstories",
  283. factory: () => new TopStoriesFeed(PREFS_CONFIG.get("discoverystream.config")),
  284. title: "Fetches content recommendations from a configurable content provider",
  285. // Dynamically determine if Pocket should be shown for a geo / locale
  286. getValue: ({geo, locale}) => {
  287. const locales = ({
  288. "US": ["en-CA", "en-GB", "en-US", "en-ZA"],
  289. "CA": ["en-CA", "en-GB", "en-US", "en-ZA"],
  290. "DE": ["de", "de-DE", "de-AT", "de-CH"],
  291. })[geo];
  292. return false;
  293. },
  294. },
  295. {
  296. name: "systemtick",
  297. factory: () => new SystemTickFeed(),
  298. title: "Produces system tick events to periodically check for data expiry",
  299. value: true,
  300. },
  301. {
  302. name: "telemetry",
  303. factory: () => new TelemetryFeed(),
  304. title: "Relays telemetry-related actions to PingCentre",
  305. value: false,
  306. },
  307. {
  308. name: "favicon",
  309. factory: () => new FaviconFeed(),
  310. title: "Fetches tippy top manifests from remote service",
  311. value: true,
  312. },
  313. {
  314. name: "topsites",
  315. factory: () => new TopSitesFeed(),
  316. title: "Queries places and gets metadata for Top Sites section",
  317. value: true,
  318. },
  319. {
  320. name: "asrouterfeed",
  321. factory: () => new ASRouterFeed(),
  322. title: "Handles AS Router messages, such as snippets and onboaridng",
  323. value: true,
  324. },
  325. {
  326. name: "discoverystreamfeed",
  327. factory: () => new DiscoveryStreamFeed(),
  328. title: "Handles new pocket ui for the new tab page",
  329. value: true,
  330. },
  331. ];
  332. const FEEDS_CONFIG = new Map();
  333. for (const config of FEEDS_DATA) {
  334. const pref = `feeds.${config.name}`;
  335. FEEDS_CONFIG.set(pref, config.factory);
  336. PREFS_CONFIG.set(pref, config);
  337. }
  338. this.ActivityStream = class ActivityStream {
  339. /**
  340. * constructor - Initializes an instance of ActivityStream
  341. */
  342. constructor() {
  343. this.initialized = false;
  344. this.store = new Store();
  345. this.feeds = FEEDS_CONFIG;
  346. this._defaultPrefs = new DefaultPrefs(PREFS_CONFIG);
  347. }
  348. init() {
  349. try {
  350. this._updateDynamicPrefs();
  351. this._defaultPrefs.init();
  352. // Hook up the store and let all feeds and pages initialize
  353. this.store.init(this.feeds, ac.BroadcastToContent({
  354. type: at.INIT,
  355. data: {},
  356. }), {type: at.UNINIT});
  357. this.initialized = true;
  358. } catch (e) {
  359. // TelemetryFeed could be unavailable if the telemetry is disabled, or
  360. // the telemetry feed is not yet initialized.
  361. const telemetryFeed = this.store.feeds.get("feeds.telemetry");
  362. if (telemetryFeed) {
  363. telemetryFeed.handleUndesiredEvent({data: {event: "ADDON_INIT_FAILED"}});
  364. }
  365. throw e;
  366. }
  367. }
  368. /**
  369. * Check if an old pref has a custom value to migrate. Clears the pref so that
  370. * it's the default after migrating (to avoid future need to migrate).
  371. *
  372. * @param oldPrefName {string} Pref to check and migrate
  373. * @param cbIfNotDefault {function} Callback that gets the current pref value
  374. */
  375. _migratePref(oldPrefName, cbIfNotDefault) {
  376. // Nothing to do if the user doesn't have a custom value
  377. if (!Services.prefs.prefHasUserValue(oldPrefName)) {
  378. return;
  379. }
  380. // Figure out what kind of pref getter to use
  381. let prefGetter;
  382. switch (Services.prefs.getPrefType(oldPrefName)) {
  383. case Services.prefs.PREF_BOOL:
  384. prefGetter = "getBoolPref";
  385. break;
  386. case Services.prefs.PREF_INT:
  387. prefGetter = "getIntPref";
  388. break;
  389. case Services.prefs.PREF_STRING:
  390. prefGetter = "getStringPref";
  391. break;
  392. }
  393. // Give the callback the current value then clear the pref
  394. cbIfNotDefault(Services.prefs[prefGetter](oldPrefName));
  395. Services.prefs.clearUserPref(oldPrefName);
  396. }
  397. uninit() {
  398. if (this.geo === "") {
  399. Services.prefs.removeObserver(GEO_PREF, this);
  400. }
  401. this.store.uninit();
  402. this.initialized = false;
  403. }
  404. _updateDynamicPrefs() {
  405. // Save the geo pref if we have it
  406. if (Services.prefs.prefHasUserValue(GEO_PREF)) {
  407. this.geo = Services.prefs.getStringPref(GEO_PREF);
  408. } else if (this.geo !== "") {
  409. // Watch for geo changes and use a dummy value for now
  410. Services.prefs.addObserver(GEO_PREF, this);
  411. this.geo = "";
  412. }
  413. this.locale = Services.locale.appLocaleAsLangTag;
  414. // Update the pref config of those with dynamic values
  415. for (const pref of PREFS_CONFIG.keys()) {
  416. const prefConfig = PREFS_CONFIG.get(pref);
  417. if (!prefConfig.getValue) {
  418. continue;
  419. }
  420. const newValue = prefConfig.getValue({
  421. geo: this.geo,
  422. locale: this.locale,
  423. });
  424. // If there's an existing value and it has changed, that means we need to
  425. // overwrite the default with the new value.
  426. if (prefConfig.value !== undefined && prefConfig.value !== newValue) {
  427. this._defaultPrefs.set(pref, newValue);
  428. }
  429. prefConfig.value = newValue;
  430. }
  431. }
  432. observe(subject, topic, data) {
  433. switch (topic) {
  434. case "nsPref:changed":
  435. // We should only expect one geo change, so update and stop observing
  436. if (data === GEO_PREF) {
  437. this._updateDynamicPrefs();
  438. Services.prefs.removeObserver(GEO_PREF, this);
  439. }
  440. break;
  441. }
  442. }
  443. };
  444. const EXPORTED_SYMBOLS = ["ActivityStream", "PREFS_CONFIG"];