diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 05:35:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 05:35:29 +0000 |
commit | 59203c63bb777a3bacec32fb8830fba33540e809 (patch) | |
tree | 58298e711c0ff0575818c30485b44a2f21bf28a0 /browser/components/newtab/lib | |
parent | Adding upstream version 126.0.1. (diff) | |
download | firefox-59203c63bb777a3bacec32fb8830fba33540e809.tar.xz firefox-59203c63bb777a3bacec32fb8830fba33540e809.zip |
Adding upstream version 127.0.upstream/127.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/newtab/lib')
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®ion=$region&count=30`; + )}/desktop/v1/recommendations?locale=$locale®ion=$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; +}; |