summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/lib
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/lib')
-rw-r--r--browser/components/newtab/lib/AboutPreferences.sys.mjs34
-rw-r--r--browser/components/newtab/lib/ActivityStream.sys.mjs61
-rw-r--r--browser/components/newtab/lib/ActivityStreamStorage.sys.mjs9
-rw-r--r--browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs9
-rw-r--r--browser/components/newtab/lib/DownloadsManager.sys.mjs8
-rw-r--r--browser/components/newtab/lib/PlacesFeed.sys.mjs3
-rw-r--r--browser/components/newtab/lib/TelemetryFeed.sys.mjs79
-rw-r--r--browser/components/newtab/lib/TopSitesFeed.sys.mjs4
-rw-r--r--browser/components/newtab/lib/WeatherFeed.sys.mjs208
9 files changed, 398 insertions, 17 deletions
diff --git a/browser/components/newtab/lib/AboutPreferences.sys.mjs b/browser/components/newtab/lib/AboutPreferences.sys.mjs
index 08e0ca422a..7d13214361 100644
--- a/browser/components/newtab/lib/AboutPreferences.sys.mjs
+++ b/browser/components/newtab/lib/AboutPreferences.sys.mjs
@@ -50,6 +50,26 @@ const PREFS_BEFORE_SECTIONS = () => [
rowsPref: "topSitesRows",
eventSource: "TOP_SITES",
},
+ {
+ id: "weather",
+ icon: "chrome://browser/skin/weather/sunny.svg",
+ pref: {
+ feed: "showWeather",
+ titleString: "home-prefs-weather-header",
+ descString: "home-prefs-weather-description",
+ learnMore: {
+ link: {
+ href: "https://support.mozilla.org/kb/customize-items-on-firefox-new-tab-page",
+ id: "home-prefs-weather-learn-more-link",
+ },
+ },
+ },
+ eventSource: "WEATHER",
+ shouldHidePref: !Services.prefs.getBoolPref(
+ "browser.newtabpage.activity-stream.system.showWeather",
+ false
+ ),
+ },
];
export class AboutPreferences {
@@ -74,7 +94,7 @@ export class AboutPreferences {
break;
// This is used to open the web extension settings page for an extension
case at.OPEN_WEBEXT_SETTINGS:
- action._target.browser.ownerGlobal.BrowserOpenAddonsMgr(
+ action._target.browser.ownerGlobal.BrowserAddonUI.openAddonsMgr(
`addons://detail/${encodeURIComponent(action.data)}`
);
break;
@@ -213,15 +233,13 @@ export class AboutPreferences {
linkPref(checkbox, name, "bool");
- // Specially add a link for stories
- if (id === "topstories") {
- const sponsoredHbox = createAppend("hbox", sectionVbox);
- sponsoredHbox.setAttribute("align", "center");
- sponsoredHbox.appendChild(checkbox);
+ // Specially add a link for Recommended stories and Weather
+ if (id === "topstories" || id === "weather") {
+ const hboxWithLink = createAppend("hbox", sectionVbox);
+ hboxWithLink.appendChild(checkbox);
checkbox.classList.add("tail-with-learn-more");
- const link = createAppend("label", sponsoredHbox, { is: "text-link" });
- link.classList.add("learn-sponsored");
+ const link = createAppend("label", hboxWithLink, { is: "text-link" });
link.setAttribute("href", sectionData.pref.learnMore.link.href);
document.l10n.setAttributes(link, sectionData.pref.learnMore.link.id);
}
diff --git a/browser/components/newtab/lib/ActivityStream.sys.mjs b/browser/components/newtab/lib/ActivityStream.sys.mjs
index fa2d011f11..430707ab5b 100644
--- a/browser/components/newtab/lib/ActivityStream.sys.mjs
+++ b/browser/components/newtab/lib/ActivityStream.sys.mjs
@@ -37,6 +37,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
TopSitesFeed: "resource://activity-stream/lib/TopSitesFeed.sys.mjs",
TopStoriesFeed: "resource://activity-stream/lib/TopStoriesFeed.sys.mjs",
WallpaperFeed: "resource://activity-stream/lib/WallpaperFeed.sys.mjs",
+ WeatherFeed: "resource://activity-stream/lib/WeatherFeed.sys.mjs",
});
// NB: Eagerly load modules that will be loaded/constructed/initialized in the
@@ -57,6 +58,16 @@ function showSpocs({ geo }) {
return spocsGeo.includes(geo);
}
+function showWeather({ geo }) {
+ const weatherGeoString =
+ lazy.NimbusFeatures.pocketNewtab.getVariable("regionWeatherConfig") || "";
+ const weatherGeo = weatherGeoString
+ .split(",")
+ .map(s => s.trim())
+ .filter(item => item);
+ return weatherGeo.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.
export const PREFS_CONFIG = new Map([
@@ -132,6 +143,50 @@ export const PREFS_CONFIG = new Map([
},
],
[
+ "system.showWeather",
+ {
+ title: "system.showWeather",
+ // pref is dynamic
+ getValue: showWeather,
+ },
+ ],
+ [
+ "showWeather",
+ {
+ title: "showWeather",
+ value: true,
+ },
+ ],
+ [
+ "weather.query",
+ {
+ title: "weather.query",
+ value: "",
+ },
+ ],
+ [
+ "weather.locationSearchEnabled",
+ {
+ title: "Enable the option to search for a specific city",
+ value: false,
+ },
+ ],
+ [
+ "weather.temperatureUnits",
+ {
+ title: "Switch the temperature between Celsius and Fahrenheit",
+ value: "f",
+ },
+ ],
+ [
+ "weather.display",
+ {
+ title:
+ "Toggle the weather widget to include a text summary of the current conditions",
+ value: "simple",
+ },
+ ],
+ [
"pocketCta",
{
title: "Pocket cta and button for logged out users.",
@@ -552,6 +607,12 @@ const FEEDS_DATA = [
title: "Handles fetching and managing wallpaper data from RemoteSettings",
value: true,
},
+ {
+ name: "weatherfeed",
+ factory: () => new lazy.WeatherFeed(),
+ title: "Handles fetching and caching weather data",
+ value: true,
+ },
];
const FEEDS_CONFIG = new Map();
diff --git a/browser/components/newtab/lib/ActivityStreamStorage.sys.mjs b/browser/components/newtab/lib/ActivityStreamStorage.sys.mjs
index 1e128ec3f2..22a1dea2a9 100644
--- a/browser/components/newtab/lib/ActivityStreamStorage.sys.mjs
+++ b/browser/components/newtab/lib/ActivityStreamStorage.sys.mjs
@@ -38,6 +38,7 @@ export class ActivityStreamStorage {
return {
get: this._get.bind(this, storeName),
getAll: this._getAll.bind(this, storeName),
+ getAllKeys: this._getAllKeys.bind(this, storeName),
set: this._set.bind(this, storeName),
};
}
@@ -61,6 +62,12 @@ export class ActivityStreamStorage {
);
}
+ _getAllKeys(storeName) {
+ return this._requestWrapper(async () =>
+ (await this._getStore(storeName)).getAllKeys()
+ );
+ }
+
_set(storeName, key, value) {
return this._requestWrapper(async () =>
(await this._getStore(storeName)).put(value, key)
@@ -68,7 +75,7 @@ export class ActivityStreamStorage {
}
_openDatabase() {
- return lazy.IndexedDB.open(this.dbName, { version: this.dbVersion }, db => {
+ return lazy.IndexedDB.open(this.dbName, this.dbVersion, db => {
// If provided with array of objectStore names we need to create all the
// individual stores
this.storeNames.forEach(store => {
diff --git a/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs b/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs
index bff9f1e04e..e1f5dff6ce 100644
--- a/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs
+++ b/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs
@@ -564,10 +564,17 @@ export class DiscoveryStreamFeed {
}
generateFeedUrl(isBff) {
+ // check for experiment parameters
+ const hasParameters = lazy.NimbusFeatures.pocketNewtab.getVariable(
+ "pocketFeedParameters"
+ );
+
if (isBff) {
return `https://${Services.prefs.getStringPref(
"extensions.pocket.bffApi"
- )}/desktop/v1/recommendations?locale=$locale&region=$region&count=30`;
+ )}/desktop/v1/recommendations?locale=$locale&region=$region&count=30${
+ hasParameters || ""
+ }`;
}
return FEED_URL;
}
diff --git a/browser/components/newtab/lib/DownloadsManager.sys.mjs b/browser/components/newtab/lib/DownloadsManager.sys.mjs
index f6e99e462a..3646ebc73a 100644
--- a/browser/components/newtab/lib/DownloadsManager.sys.mjs
+++ b/browser/components/newtab/lib/DownloadsManager.sys.mjs
@@ -7,6 +7,7 @@ import { actionTypes as at } from "resource://activity-stream/common/Actions.mjs
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs",
DownloadsViewUI: "resource:///modules/DownloadsViewUI.sys.mjs",
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
@@ -166,10 +167,8 @@ export class DownloadsManager {
);
});
break;
- case at.OPEN_DOWNLOAD_FILE:
- const win = action._target.browser.ownerGlobal;
- const openWhere =
- action.data.event && win.whereToOpenLink(action.data.event);
+ case at.OPEN_DOWNLOAD_FILE: {
+ const openWhere = lazy.BrowserUtils.whereToOpenLink(action.data.event);
doDownloadAction(download => {
lazy.DownloadsCommon.openDownload(download, {
// Replace "current" or unknown value with "tab" as the default behavior
@@ -180,6 +179,7 @@ export class DownloadsManager {
});
});
break;
+ }
case at.UNINIT:
this.uninit();
break;
diff --git a/browser/components/newtab/lib/PlacesFeed.sys.mjs b/browser/components/newtab/lib/PlacesFeed.sys.mjs
index 85679153bd..78e6873b3d 100644
--- a/browser/components/newtab/lib/PlacesFeed.sys.mjs
+++ b/browser/components/newtab/lib/PlacesFeed.sys.mjs
@@ -24,6 +24,7 @@ const { AboutNewTab } = ChromeUtils.importESModule(
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
@@ -274,7 +275,7 @@ export class PlacesFeed {
const win = action._target.browser.ownerGlobal;
win.openTrustedLinkIn(
urlToOpen,
- where || win.whereToOpenLink(event),
+ where || lazy.BrowserUtils.whereToOpenLink(event),
params
);
diff --git a/browser/components/newtab/lib/TelemetryFeed.sys.mjs b/browser/components/newtab/lib/TelemetryFeed.sys.mjs
index 6cf4dba4ab..2643337674 100644
--- a/browser/components/newtab/lib/TelemetryFeed.sys.mjs
+++ b/browser/components/newtab/lib/TelemetryFeed.sys.mjs
@@ -114,6 +114,7 @@ const NEWTAB_PING_PREFS = {
"feeds.section.topstories": Glean.pocket.enabled,
showSponsored: Glean.pocket.sponsoredStoriesEnabled,
topSitesRows: Glean.topsites.rows,
+ showWeather: Glean.newtab.weatherEnabled,
};
const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors";
@@ -932,9 +933,87 @@ export class TelemetryFeed {
case at.BLOCK_URL:
this.handleBlockUrl(action);
break;
+ case at.WALLPAPER_CLICK:
+ this.handleWallpaperUserEvent(action);
+ break;
+ case at.SET_PREF:
+ this.handleSetPref(action);
+ break;
+ case at.WEATHER_IMPRESSION:
+ this.handleWeatherUserEvent(action);
+ break;
+ case at.WEATHER_LOAD_ERROR:
+ this.handleWeatherUserEvent(action);
+ break;
+ case at.WEATHER_OPEN_PROVIDER_URL:
+ this.handleWeatherUserEvent(action);
+ break;
+ }
+ }
+
+ handleSetPref(action) {
+ const prefName = action.data.name;
+
+ // TODO: Migrate this event to handleWeatherUserEvent()
+ if (prefName === "weather.display") {
+ const session = this.sessions.get(au.getPortIdOfSender(action));
+
+ if (!session) {
+ return;
+ }
+
+ Glean.newtab.weatherChangeDisplay.record({
+ newtab_visit_id: session.session_id,
+ weather_display_mode: action.data.value,
+ });
+ }
+ }
+
+ handleWeatherUserEvent(action) {
+ const session = this.sessions.get(au.getPortIdOfSender(action));
+
+ if (!session) {
+ return;
+ }
+
+ // Weather specific telemtry events can be added and parsed here.
+ switch (action.type) {
+ case "WEATHER_IMPRESSION":
+ Glean.newtab.weatherImpression.record({
+ newtab_visit_id: session.session_id,
+ });
+ break;
+ case "WEATHER_LOAD_ERROR":
+ Glean.newtab.weatherLoadError.record({
+ newtab_visit_id: session.session_id,
+ });
+ break;
+ case "WEATHER_OPEN_PROVIDER_URL":
+ Glean.newtab.weatherOpenProviderUrl.record({
+ newtab_visit_id: session.session_id,
+ });
+ break;
+ default:
+ break;
}
}
+ handleWallpaperUserEvent(action) {
+ const session = this.sessions.get(au.getPortIdOfSender(action));
+
+ if (!session) {
+ return;
+ }
+ const { data } = action;
+ const { selected_wallpaper, hadPreviousWallpaper } = data;
+ // if either of the wallpaper prefs are truthy, they had a previous wallpaper
+ Glean.newtab.wallpaperClick.record({
+ newtab_visit_id: session.session_id,
+ selected_wallpaper,
+ hadPreviousWallpaper,
+ });
+ }
+
handleBlockUrl(action) {
const session = this.sessions.get(au.getPortIdOfSender(action));
// TODO: Do we want to not send this unless there's a newtab_visit_id?
diff --git a/browser/components/newtab/lib/TopSitesFeed.sys.mjs b/browser/components/newtab/lib/TopSitesFeed.sys.mjs
index e259253402..7ab85466c6 100644
--- a/browser/components/newtab/lib/TopSitesFeed.sys.mjs
+++ b/browser/components/newtab/lib/TopSitesFeed.sys.mjs
@@ -73,7 +73,7 @@ const ROWS_PREF = "topSitesRows";
const SHOW_SPONSORED_PREF = "showSponsoredTopSites";
// The default total number of sponsored top sites to fetch from Contile
// and Pocket.
-const MAX_NUM_SPONSORED = 2;
+const MAX_NUM_SPONSORED = 3;
// Nimbus variable for the total number of sponsored top sites including
// both Contile and Pocket sources.
// The default will be `MAX_NUM_SPONSORED` if this variable is unspecified.
@@ -112,7 +112,7 @@ const NIMBUS_VARIABLE_CONTILE_POSITIONS = "contileTopsitesPositions";
const CONTILE_ENDPOINT_PREF = "browser.topsites.contile.endpoint";
const CONTILE_UPDATE_INTERVAL = 15 * 60 * 1000; // 15 minutes
// The maximum number of sponsored top sites to fetch from Contile.
-const CONTILE_MAX_NUM_SPONSORED = 2;
+const CONTILE_MAX_NUM_SPONSORED = 3;
const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors";
const CONTILE_CACHE_PREF = "browser.topsites.contile.cachedTiles";
const CONTILE_CACHE_VALID_FOR_PREF = "browser.topsites.contile.cacheValidFor";
diff --git a/browser/components/newtab/lib/WeatherFeed.sys.mjs b/browser/components/newtab/lib/WeatherFeed.sys.mjs
new file mode 100644
index 0000000000..16aa8196af
--- /dev/null
+++ b/browser/components/newtab/lib/WeatherFeed.sys.mjs
@@ -0,0 +1,208 @@
+/* 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/. */
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ MerinoClient: "resource:///modules/MerinoClient.sys.mjs",
+ clearTimeout: "resource://gre/modules/Timer.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+ PersistentCache: "resource://activity-stream/lib/PersistentCache.sys.mjs",
+});
+
+import {
+ actionTypes as at,
+ actionCreators as ac,
+} from "resource://activity-stream/common/Actions.mjs";
+
+const CACHE_KEY = "weather_feed";
+const WEATHER_UPDATE_TIME = 10 * 60 * 1000; // 10 minutes
+const MERINO_PROVIDER = "accuweather";
+
+const PREF_WEATHER_QUERY = "weather.query";
+const PREF_SHOW_WEATHER = "showWeather";
+const PREF_SYSTEM_SHOW_WEATHER = "system.showWeather";
+
+/**
+ * A feature that periodically fetches weather suggestions from Merino for HNT.
+ */
+export class WeatherFeed {
+ constructor() {
+ this.loaded = false;
+ this.merino = null;
+ this.suggestions = [];
+ this.lastUpdated = null;
+ this.fetchTimer = null;
+ this.fetchIntervalMs = 30 * 60 * 1000; // 30 minutes
+ this.timeoutMS = 5000;
+ this.lastFetchTimeMs = 0;
+ this.fetchDelayAfterComingOnlineMs = 3000; // 3s
+ this.cache = this.PersistentCache(CACHE_KEY, true);
+ }
+
+ async resetCache() {
+ if (this.cache) {
+ await this.cache.set("weather", {});
+ }
+ }
+
+ async resetWeather() {
+ await this.resetCache();
+ this.suggestions = [];
+ this.lastUpdated = null;
+ }
+
+ isEnabled() {
+ return (
+ this.store.getState().Prefs.values[PREF_SHOW_WEATHER] &&
+ this.store.getState().Prefs.values[PREF_SYSTEM_SHOW_WEATHER]
+ );
+ }
+
+ async init() {
+ await this.loadWeather(true /* isStartup */);
+ }
+
+ stopFetching() {
+ if (!this.merino) {
+ return;
+ }
+
+ lazy.clearTimeout(this.fetchTimer);
+ this.merino = null;
+ this.suggestions = null;
+ this.fetchTimer = 0;
+ }
+
+ /**
+ * This thin wrapper around the fetch call makes it easier for us to write
+ * automated tests that simulate responses.
+ */
+ async fetchHelper() {
+ this.restartFetchTimer();
+ const weatherQuery = this.store.getState().Prefs.values[PREF_WEATHER_QUERY];
+ let suggestions = [];
+ try {
+ suggestions = await this.merino.fetch({
+ query: weatherQuery || "",
+ providers: [MERINO_PROVIDER],
+ timeoutMs: 5000,
+ });
+ } catch (error) {
+ // We don't need to do anything with this right now.
+ }
+
+ // results from the API or empty array if null
+ this.suggestions = suggestions ?? [];
+ }
+
+ async fetch(isStartup) {
+ // Keep a handle on the `MerinoClient` instance that exists at the start of
+ // this fetch. If fetching stops or this `Weather` instance is uninitialized
+ // during the fetch, `#merino` will be nulled, and the fetch should stop. We
+ // can compare `merino` to `this.merino` to tell when this occurs.
+ this.merino = await this.MerinoClient("HNT_WEATHER_FEED");
+ await this.fetchHelper();
+
+ if (this.suggestions.length) {
+ this.lastUpdated = this.Date().now();
+ await this.cache.set("weather", {
+ suggestions: this.suggestions,
+ lastUpdated: this.lastUpdated,
+ });
+ }
+
+ this.update(isStartup);
+ }
+
+ async loadWeather(isStartup = false) {
+ const cachedData = (await this.cache.get()) || {};
+ const { weather } = cachedData;
+
+ // If we have nothing in cache, or cache has expired, we can make a fresh fetch.
+ if (
+ !weather?.lastUpdated ||
+ !(this.Date().now() - weather.lastUpdated < WEATHER_UPDATE_TIME)
+ ) {
+ await this.fetch(isStartup);
+ } else if (!this.lastUpdated) {
+ this.suggestions = weather.suggestions;
+ this.lastUpdated = weather.lastUpdated;
+ this.update(isStartup);
+ }
+ }
+
+ update(isStartup) {
+ this.store.dispatch(
+ ac.BroadcastToContent({
+ type: at.WEATHER_UPDATE,
+ data: {
+ suggestions: this.suggestions,
+ lastUpdated: this.lastUpdated,
+ },
+ meta: {
+ isStartup,
+ },
+ })
+ );
+ }
+
+ restartFetchTimer(ms = this.fetchIntervalMs) {
+ lazy.clearTimeout(this.fetchTimer);
+ this.fetchTimer = lazy.setTimeout(() => {
+ this.fetch();
+ }, ms);
+ }
+
+ async onPrefChangedAction(action) {
+ switch (action.data.name) {
+ case PREF_WEATHER_QUERY:
+ await this.loadWeather();
+ break;
+ case PREF_SHOW_WEATHER:
+ case PREF_SYSTEM_SHOW_WEATHER:
+ if (this.isEnabled() && action.data.value) {
+ await this.loadWeather();
+ } else {
+ await this.resetWeather();
+ }
+ break;
+ }
+ }
+
+ async onAction(action) {
+ switch (action.type) {
+ case at.INIT:
+ if (this.isEnabled()) {
+ await this.init();
+ }
+ break;
+ case at.UNINIT:
+ await this.resetWeather();
+ break;
+ case at.DISCOVERY_STREAM_DEV_SYSTEM_TICK:
+ case at.SYSTEM_TICK:
+ if (this.isEnabled()) {
+ await this.loadWeather();
+ }
+ break;
+ case at.PREF_CHANGED:
+ await this.onPrefChangedAction(action);
+ break;
+ }
+ }
+}
+
+/**
+ * Creating a thin wrapper around MerinoClient, PersistentCache, and Date.
+ * This makes it easier for us to write automated tests that simulate responses.
+ */
+WeatherFeed.prototype.MerinoClient = (...args) => {
+ return new lazy.MerinoClient(...args);
+};
+WeatherFeed.prototype.PersistentCache = (...args) => {
+ return new lazy.PersistentCache(...args);
+};
+WeatherFeed.prototype.Date = () => {
+ return Date;
+};