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 | |
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')
52 files changed, 4002 insertions, 241 deletions
diff --git a/browser/components/newtab/AboutNewTabService.sys.mjs b/browser/components/newtab/AboutNewTabService.sys.mjs index 73502fcb4f..37adc25f6e 100644 --- a/browser/components/newtab/AboutNewTabService.sys.mjs +++ b/browser/components/newtab/AboutNewTabService.sys.mjs @@ -109,7 +109,11 @@ export const AboutHomeStartupCacheChild = { ); } - if (!lazy.NimbusFeatures.abouthomecache.getVariable("enabled")) { + if ( + !Services.prefs.getBoolPref( + "browser.startup.homepage.abouthome_cache.enabled" + ) + ) { return; } diff --git a/browser/components/newtab/common/Actions.mjs b/browser/components/newtab/common/Actions.mjs index 7273d80220..a86a1d1e81 100644 --- a/browser/components/newtab/common/Actions.mjs +++ b/browser/components/newtab/common/Actions.mjs @@ -161,6 +161,11 @@ for (const type of [ "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", "WALLPAPERS_SET", + "WALLPAPER_CLICK", + "WEATHER_IMPRESSION", + "WEATHER_LOAD_ERROR", + "WEATHER_OPEN_PROVIDER_URL", + "WEATHER_UPDATE", "WEBEXT_CLICK", "WEBEXT_DISMISS", ]) { diff --git a/browser/components/newtab/common/Reducers.sys.mjs b/browser/components/newtab/common/Reducers.sys.mjs index 326217538d..edd3668434 100644 --- a/browser/components/newtab/common/Reducers.sys.mjs +++ b/browser/components/newtab/common/Reducers.sys.mjs @@ -104,6 +104,12 @@ export const INITIAL_STATE = { Wallpapers: { wallpaperList: [], }, + Weather: { + // do we have the data from WeatherFeed yet? + initialized: false, + suggestions: [], + lastUpdated: null, + }, }; function App(prevState = INITIAL_STATE.App, action) { @@ -853,6 +859,20 @@ function Wallpapers(prevState = INITIAL_STATE.Wallpapers, action) { } } +function Weather(prevState = INITIAL_STATE.Weather, action) { + switch (action.type) { + case at.WEATHER_UPDATE: + return { + ...prevState, + suggestions: action.data.suggestions, + lastUpdated: action.data.date, + initialized: true, + }; + default: + return prevState; + } +} + export const reducers = { TopSites, App, @@ -865,4 +885,5 @@ export const reducers = { DiscoveryStream, Search, Wallpapers, + Weather, }; diff --git a/browser/components/newtab/content-src/components/Base/Base.jsx b/browser/components/newtab/content-src/components/Base/Base.jsx index 1738f8f51a..61722fd418 100644 --- a/browser/components/newtab/content-src/components/Base/Base.jsx +++ b/browser/components/newtab/content-src/components/Base/Base.jsx @@ -12,6 +12,7 @@ import { CustomizeMenu } from "content-src/components/CustomizeMenu/CustomizeMen import React from "react"; import { Search } from "content-src/components/Search/Search"; import { Sections } from "content-src/components/Sections/Sections"; +import { Weather } from "content-src/components/Weather/Weather"; const VISIBLE = "visible"; const VISIBILITY_CHANGE_EVENT = "visibilitychange"; @@ -235,7 +236,7 @@ export class BaseContent extends React.PureComponent { return ( <p className={`wallpaper-attribution`} - key={name} + key={name.string} data-l10n-id="newtab-wallpaper-attribution" data-l10n-args={JSON.stringify({ author_string: name.string, @@ -278,6 +279,15 @@ export class BaseContent extends React.PureComponent { `--newtab-wallpaper-dark`, `url(${darkWallpaper?.wallpaperUrl || ""})` ); + + // Add helper class to body if user has a wallpaper selected + if (lightWallpaper) { + global.document?.body.classList.add("hasWallpaperLight"); + } + + if (darkWallpaper) { + global.document?.body.classList.add("hasWallpaperDark"); + } } } @@ -290,6 +300,7 @@ export class BaseContent extends React.PureComponent { const activeWallpaper = prefs[`newtabWallpapers.wallpaper-${this.state.colorMode}`]; const wallpapersEnabled = prefs["newtabWallpapers.enabled"]; + const weatherEnabled = prefs.showWeather; const { pocketConfig } = prefs; @@ -322,10 +333,12 @@ export class BaseContent extends React.PureComponent { showSponsoredPocketEnabled: prefs.showSponsored, showRecentSavesEnabled: prefs.showRecentSaves, topSitesRowsCount: prefs.topSitesRows, + weatherEnabled: prefs.showWeather, }; const pocketRegion = prefs["feeds.system.topstories"]; const mayHaveSponsoredStories = prefs["system.showSponsored"]; + const mayHaveWeather = prefs["system.showWeather"]; const { mayHaveSponsoredTopSites } = prefs; const outerClassName = [ @@ -358,6 +371,7 @@ export class BaseContent extends React.PureComponent { pocketRegion={pocketRegion} mayHaveSponsoredTopSites={mayHaveSponsoredTopSites} mayHaveSponsoredStories={mayHaveSponsoredStories} + mayHaveWeather={mayHaveWeather} spocMessageVariant={spocMessageVariant} showing={customizeMenuVisible} /> @@ -393,6 +407,13 @@ export class BaseContent extends React.PureComponent { <ConfirmDialog /> {wallpapersEnabled && this.renderWallpaperAttribution()} </main> + <aside> + {weatherEnabled && ( + <ErrorBoundary> + <Weather /> + </ErrorBoundary> + )} + </aside> </div> </div> ); @@ -410,4 +431,5 @@ export const Base = connect(state => ({ DiscoveryStream: state.DiscoveryStream, Search: state.Search, Wallpapers: state.Wallpapers, + Weather: state.Weather, }))(_Base); diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx b/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx index 1dd13fc965..494d506da9 100644 --- a/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx +++ b/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx @@ -28,7 +28,7 @@ export class ContentSection extends React.PureComponent { } onPreferenceSelect(e) { - // eventSource: TOP_SITES | TOP_STORIES | HIGHLIGHTS + // eventSource: TOP_SITES | TOP_STORIES | HIGHLIGHTS | WEATHER const { preference, eventSource } = e.target.dataset; let value; if (e.target.nodeName === "SELECT") { @@ -97,6 +97,7 @@ export class ContentSection extends React.PureComponent { pocketRegion, mayHaveSponsoredStories, mayHaveRecentSaves, + mayHaveWeather, openPreferences, spocMessageVariant, wallpapersEnabled, @@ -107,6 +108,7 @@ export class ContentSection extends React.PureComponent { topSitesEnabled, pocketEnabled, highlightsEnabled, + weatherEnabled, showSponsoredTopSitesEnabled, showSponsoredPocketEnabled, showRecentSavesEnabled, @@ -269,6 +271,22 @@ export class ContentSection extends React.PureComponent { </label> </div> + {mayHaveWeather && ( + <div id="weather-section" className="section"> + <label className="switch"> + <moz-toggle + id="weather-toggle" + pressed={weatherEnabled || null} + onToggle={this.onPreferenceSelect} + data-preference="showWeather" + data-eventSource="WEATHER" + data-l10n-id="newtab-custom-weather-toggle" + data-l10n-attrs="label, description" + /> + </label> + </div> + )} + {pocketRegion && mayHaveSponsoredStories && spocMessageVariant === "variant-c" && ( diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx b/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx index f1c723fed2..035e84af58 100644 --- a/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx +++ b/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx @@ -55,12 +55,14 @@ export class _CustomizeMenu extends React.PureComponent { role="dialog" data-l10n-id="newtab-personalize-dialog-label" > - <button - onClick={() => this.props.onClose()} - className="close-button" - data-l10n-id="newtab-custom-close-button" - ref={c => (this.closeButton = c)} - /> + <div className="close-button-wrapper"> + <button + onClick={() => this.props.onClose()} + className="close-button" + data-l10n-id="newtab-custom-close-button" + ref={c => (this.closeButton = c)} + /> + </div> <ContentSection openPreferences={this.props.openPreferences} setPref={this.props.setPref} @@ -71,6 +73,7 @@ export class _CustomizeMenu extends React.PureComponent { mayHaveSponsoredTopSites={this.props.mayHaveSponsoredTopSites} mayHaveSponsoredStories={this.props.mayHaveSponsoredStories} mayHaveRecentSaves={this.props.DiscoveryStream.recentSavesEnabled} + mayHaveWeather={this.props.mayHaveWeather} spocMessageVariant={this.props.spocMessageVariant} dispatch={this.props.dispatch} /> diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss b/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss index c20da5ce50..403a62a50f 100644 --- a/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss +++ b/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss @@ -47,7 +47,8 @@ inset-block: 0; inset-inline-end: 0; z-index: 1001; - padding: 16px; + padding-block: 0 var(--space-large); + padding-inline: var(--space-large); overflow: auto; transform: translateX(435px); visibility: hidden; @@ -85,9 +86,17 @@ box-shadow: $shadow-large; } + .close-button-wrapper { + position: sticky; + top: 0; + padding-block-start: var(--space-large); + background-color: var(--newtab-background-color-secondary); + z-index: 1; + } + .close-button { margin-inline-start: auto; - margin-bottom: 28px; + margin-inline-end: var(--space-large); white-space: nowrap; display: block; background-color: var(--newtab-element-secondary-color); @@ -117,7 +126,7 @@ grid-template-columns: 1fr; grid-template-rows: repeat(4, auto); grid-row-gap: 32px; - padding: 0 16px; + padding: var(--space-large); .wallpapers-section h2 { font-size: inherit; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx index 8b9d64dfc1..79d453a7c9 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx @@ -126,8 +126,11 @@ export class DiscoveryStreamAdminUI extends React.PureComponent { this.systemTick = this.systemTick.bind(this); this.syncRemoteSettings = this.syncRemoteSettings.bind(this); this.onStoryToggle = this.onStoryToggle.bind(this); + this.handleWeatherSubmit = this.handleWeatherSubmit.bind(this); + this.handleWeatherUpdate = this.handleWeatherUpdate.bind(this); this.state = { toggledStories: {}, + weatherQuery: "", }; } @@ -182,6 +185,16 @@ export class DiscoveryStreamAdminUI extends React.PureComponent { this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_SYNC_RS); } + handleWeatherUpdate(e) { + this.setState({ weatherQuery: e.target.value || "" }); + } + + handleWeatherSubmit(e) { + e.preventDefault(); + const { weatherQuery } = this.state; + this.props.dispatch(ac.SetPref("weather.query", weatherQuery)); + } + renderComponent(width, component) { return ( <table> @@ -200,6 +213,46 @@ export class DiscoveryStreamAdminUI extends React.PureComponent { ); } + renderWeatherData() { + const { suggestions } = this.props.state.Weather; + let weatherTable; + if (suggestions) { + weatherTable = ( + <div className="weather-section"> + <form onSubmit={this.handleWeatherSubmit}> + <label htmlFor="weather-query">Weather query</label> + <input + type="text" + min="3" + max="10" + id="weather-query" + onChange={this.handleWeatherUpdate} + value={this.weatherQuery} + /> + <button type="submit">Submit</button> + </form> + <table> + <tbody> + {suggestions.map(suggestion => ( + <tr className="message-item" key={suggestion.city_name}> + <td className="message-id"> + <span> + {suggestion.city_name} <br /> + </span> + </td> + <td className="message-summary"> + <pre>{JSON.stringify(suggestion, null, 2)}</pre> + </td> + </tr> + ))} + </tbody> + </table> + </div> + ); + } + return weatherTable; + } + renderFeedData(url) { const { feeds } = this.props.state.DiscoveryStream; const feed = feeds.data[url].data; @@ -376,6 +429,8 @@ export class DiscoveryStreamAdminUI extends React.PureComponent { {this.renderSpocs()} <h3>Feeds Data</h3> {this.renderFeedsData()} + <h3>Weather Data</h3> + {this.renderWeatherData()} </div> ); } @@ -412,6 +467,7 @@ export class DiscoveryStreamAdminInner extends React.PureComponent { state={{ DiscoveryStream: this.props.DiscoveryStream, Personalization: this.props.Personalization, + Weather: this.props.Weather, }} otherPrefs={this.props.Prefs.values} dispatch={this.props.dispatch} @@ -500,4 +556,5 @@ export const DiscoveryStreamAdmin = connect(state => ({ DiscoveryStream: state.DiscoveryStream, Personalization: state.Personalization, Prefs: state.Prefs, + Weather: state.Weather, }))(_DiscoveryStreamAdmin); diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss index a01227dd3d..dcad97c917 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss +++ b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss @@ -334,4 +334,16 @@ } } } + + .weather-section { + margin-block-end: 24px; + + form { + display: flex; + + label { + margin-inline-end: 12px; + } + } + } } diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx index b3d965530d..461d54899f 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx @@ -468,57 +468,34 @@ export class _DSCard extends React.PureComponent { dispatch={this.props.dispatch} spocMessageVariant={this.props.spocMessageVariant} /> - {saveToPocketCard && ( - <div className="card-stp-button-hover-background"> - <div className="card-stp-button-position-wrapper"> - {!this.props.flightId && stpButton()} - <DSLinkMenu - id={this.props.id} - index={this.props.pos} - dispatch={this.props.dispatch} - url={this.props.url} - title={this.props.title} - source={source} - type={this.props.type} - pocket_id={this.props.pocket_id} - shim={this.props.shim} - bookmarkGuid={this.props.bookmarkGuid} - flightId={ - !this.props.is_collection ? this.props.flightId : undefined - } - showPrivacyInfo={!!this.props.flightId} - onMenuUpdate={this.onMenuUpdate} - onMenuShow={this.onMenuShow} - saveToPocketCard={saveToPocketCard} - pocket_button_enabled={pocketButtonEnabled} - isRecentSave={isRecentSave} - /> - </div> + + <div className="card-stp-button-hover-background"> + <div className="card-stp-button-position-wrapper"> + {saveToPocketCard && <>{!this.props.flightId && stpButton()}</>} + + <DSLinkMenu + id={this.props.id} + index={this.props.pos} + dispatch={this.props.dispatch} + url={this.props.url} + title={this.props.title} + source={source} + type={this.props.type} + pocket_id={this.props.pocket_id} + shim={this.props.shim} + bookmarkGuid={this.props.bookmarkGuid} + flightId={ + !this.props.is_collection ? this.props.flightId : undefined + } + showPrivacyInfo={!!this.props.flightId} + onMenuUpdate={this.onMenuUpdate} + onMenuShow={this.onMenuShow} + saveToPocketCard={saveToPocketCard} + pocket_button_enabled={pocketButtonEnabled} + isRecentSave={isRecentSave} + /> </div> - )} - {!saveToPocketCard && ( - <DSLinkMenu - id={this.props.id} - index={this.props.pos} - dispatch={this.props.dispatch} - url={this.props.url} - title={this.props.title} - source={source} - type={this.props.type} - pocket_id={this.props.pocket_id} - shim={this.props.shim} - bookmarkGuid={this.props.bookmarkGuid} - flightId={ - !this.props.is_collection ? this.props.flightId : undefined - } - showPrivacyInfo={!!this.props.flightId} - hostRef={this.contextMenuButtonHostRef} - onMenuUpdate={this.onMenuUpdate} - onMenuShow={this.onMenuShow} - pocket_button_enabled={pocketButtonEnabled} - isRecentSave={isRecentSave} - /> - )} + </div> </article> ); } diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss index 9004e609df..e5ac19b553 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss @@ -54,12 +54,6 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 100%); fill: $white; } - .context-menu-button { - position: static; - transition: none; - border-radius: 3px; - } - .context-menu-position-container { position: relative; } @@ -83,6 +77,10 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 100%); padding: 6px; white-space: nowrap; color: $white; + + &:focus-visible { + outline: 2px solid var(--newtab-button-focus-border); + } } button, @@ -95,6 +93,28 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 100%); } } + // Override note: The colors set here are intentionally static + // due to transparency issues over images. + .context-menu-button { + position: static; + transition: none; + border-radius: 3px; + background-color: var(--newtab-button-static-background); + + &:hover { + background-color: var(--newtab-button-static-hover-background); + + &:active { + background-color: var(--newtab-button-static-active-background); + } + } + + &:focus-visible { + outline: 2px solid var(--newtab-button-focus-border); + background-color: var(--newtab-button-static-focus-background); + } + } + &.last-item { .card-stp-button-hover-background { .context-menu { diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss index 54b39524d8..f726e936b9 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss @@ -37,6 +37,24 @@ a { text-decoration: none; } + + // Override note: The colors set here are intentionally static + // due to transparency issues over images. + .context-menu-button { + background-color: var(--newtab-button-static-background); + + &:hover { + background-color: var(--newtab-button-static-hover-background); + + &:active { + background-color: var(--newtab-button-static-active-background); + } + } + + &:focus-visible { + background-color: var(--newtab-button-static-focus-background); + } + } } } } diff --git a/browser/components/newtab/content-src/components/TopSites/_TopSites.scss b/browser/components/newtab/content-src/components/TopSites/_TopSites.scss index 4e4019513d..09a20c235d 100644 --- a/browser/components/newtab/content-src/components/TopSites/_TopSites.scss +++ b/browser/components/newtab/content-src/components/TopSites/_TopSites.scss @@ -126,12 +126,23 @@ $letter-fallback-color: $white; } } + // Necessary for when navigating by a keyboard, having the context + // menu open should display the "…" button. This style is a clone + // of the `:active` state for `.context-menu-button` + &.active { + .context-menu-button { + opacity: 1; + background-color: var(--newtab-button-active-background); + } + } + .context-menu-button { background-image: url('chrome://global/skin/icons/more.svg'); + background-color: var(--newtab-button-background); border: 0; border-radius: 4px; cursor: pointer; - fill: var(--newtab-text-primary-color); + fill: var(--newtab-button-text); -moz-context-properties: fill; height: 20px; width: 20px; @@ -141,11 +152,18 @@ $letter-fallback-color: $white; top: -20px; transition: opacity 200ms; - &:is(:active, :focus) { - outline: 0; + &:hover { + background-color: var(--newtab-button-hover-background); + + &:active { + background-color: var(--newtab-button-active-background); + } + } + + &:focus-visible { + background-color: var(--newtab-button-focus-background); + border-color: var(--newtab-button-focus-border); opacity: 1; - background-color: var(--newtab-element-hover-color); - fill: var(--newtab-primary-action-background); } } diff --git a/browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx b/browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx index 0b51a146f5..6fcd4b3a15 100644 --- a/browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx +++ b/browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx @@ -4,6 +4,7 @@ import React from "react"; import { connect } from "react-redux"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; export class _WallpapersSection extends React.PureComponent { constructor(props) { @@ -25,6 +26,10 @@ export class _WallpapersSection extends React.PureComponent { const prefs = this.props.Prefs.values; const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; this.props.setPref(`newtabWallpapers.wallpaper-${colorMode}`, id); + this.handleUserEvent({ + selected_wallpaper: id, + hadPreviousWallpaper: !!this.props.activeWallpaper, + }); // bug 1892095 if ( prefs["newtabWallpapers.wallpaper-dark"] === "" && @@ -50,6 +55,20 @@ export class _WallpapersSection extends React.PureComponent { handleReset() { const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; this.props.setPref(`newtabWallpapers.wallpaper-${colorMode}`, ""); + this.handleUserEvent({ + selected_wallpaper: "none", + hadPreviousWallpaper: !!this.props.activeWallpaper, + }); + } + + // Record user interaction when changing wallpaper and reseting wallpaper to default + handleUserEvent(data) { + this.props.dispatch( + ac.OnlyToMain({ + type: at.WALLPAPER_CLICK, + data, + }) + ); } render() { diff --git a/browser/components/newtab/content-src/components/Weather/Weather.jsx b/browser/components/newtab/content-src/components/Weather/Weather.jsx new file mode 100644 index 0000000000..9273f9a4bd --- /dev/null +++ b/browser/components/newtab/content-src/components/Weather/Weather.jsx @@ -0,0 +1,350 @@ +/* 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/. */ + +import { connect } from "react-redux"; +import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; +import React from "react"; + +const VISIBLE = "visible"; +const VISIBILITY_CHANGE_EVENT = "visibilitychange"; + +export class _Weather extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + contextMenuKeyboard: false, + showContextMenu: false, + url: "https://example.com", + impressionSeen: false, + errorSeen: false, + }; + this.setImpressionRef = element => { + this.impressionElement = element; + }; + this.setErrorRef = element => { + this.errorElement = element; + }; + this.onClick = this.onClick.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.onUpdate = this.onUpdate.bind(this); + this.onProviderClick = this.onProviderClick.bind(this); + } + + componentDidMount() { + const { props } = this; + + if (!props.dispatch) { + return; + } + + if (props.document.visibilityState === VISIBLE) { + // Setup the impression observer once the page is visible. + this.setImpressionObservers(); + } else { + // We should only ever send the latest impression stats ping, so remove any + // older listeners. + if (this._onVisibilityChange) { + props.document.removeEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + + this._onVisibilityChange = () => { + if (props.document.visibilityState === VISIBLE) { + // Setup the impression observer once the page is visible. + this.setImpressionObservers(); + props.document.removeEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + }; + props.document.addEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + } + + componentWillUnmount() { + // Remove observers on unmount + if (this.observer && this.impressionElement) { + this.observer.unobserve(this.impressionElement); + } + if (this.observer && this.errorElement) { + this.observer.unobserve(this.errorElement); + } + if (this._onVisibilityChange) { + this.props.document.removeEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + } + + setImpressionObservers() { + if (this.impressionElement) { + this.observer = new IntersectionObserver(this.onImpression.bind(this)); + this.observer.observe(this.impressionElement); + } + if (this.errorElement) { + this.observer = new IntersectionObserver(this.onError.bind(this)); + this.observer.observe(this.errorElement); + } + } + + onImpression(entries) { + if (this.state) { + const entry = entries.find(e => e.isIntersecting); + + if (entry) { + if (this.impressionElement) { + this.observer.unobserve(this.impressionElement); + } + + this.props.dispatch( + ac.OnlyToMain({ + type: at.WEATHER_IMPRESSION, + }) + ); + + // Stop observing since element has been seen + this.setState({ + impressionSeen: true, + }); + } + } + } + + onError(entries) { + if (this.state) { + const entry = entries.find(e => e.isIntersecting); + + if (entry) { + if (this.errorElement) { + this.observer.unobserve(this.errorElement); + } + + this.props.dispatch( + ac.OnlyToMain({ + type: at.WEATHER_LOAD_ERROR, + }) + ); + + // Stop observing since element has been seen + this.setState({ + errorSeen: true, + }); + } + } + } + + openContextMenu(isKeyBoard) { + if (this.props.onUpdate) { + this.props.onUpdate(true); + } + this.setState({ + showContextMenu: true, + contextMenuKeyboard: isKeyBoard, + }); + } + + onClick(event) { + event.preventDefault(); + this.openContextMenu(false, event); + } + + onKeyDown(event) { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + this.openContextMenu(true, event); + } + } + + onUpdate(showContextMenu) { + if (this.props.onUpdate) { + this.props.onUpdate(showContextMenu); + } + this.setState({ showContextMenu }); + } + + onProviderClick() { + this.props.dispatch( + ac.OnlyToMain({ + type: at.WEATHER_OPEN_PROVIDER_URL, + data: { + source: "WEATHER", + }, + }) + ); + } + + render() { + // Check if weather should be rendered + const isWeatherEnabled = this.props.Prefs.values["system.showWeather"]; + + if (!isWeatherEnabled || !this.props.Weather.initialized) { + return false; + } + + const { showContextMenu } = this.state; + + const WEATHER_SUGGESTION = this.props.Weather.suggestions?.[0]; + + const { + className, + index, + dispatch, + eventSource, + shouldSendImpressionStats, + } = this.props; + const { props } = this; + const isContextMenuOpen = this.state.activeCard === index; + + const outerClassName = [ + "weather", + className, + isContextMenuOpen && "active", + props.placeholder && "placeholder", + ] + .filter(v => v) + .join(" "); + + const showDetailedView = + this.props.Prefs.values["weather.display"] === "detailed"; + + // Note: The temperature units/display options will become secondary menu items + const WEATHER_SOURCE_CONTEXT_MENU_OPTIONS = [ + ...(this.props.Prefs.values["weather.locationSearchEnabled"] + ? ["ChangeWeatherLocation"] + : []), + ...(this.props.Prefs.values["weather.temperatureUnits"] === "f" + ? ["ChangeTempUnitCelsius"] + : ["ChangeTempUnitFahrenheit"]), + ...(this.props.Prefs.values["weather.display"] === "simple" + ? ["ChangeWeatherDisplayDetailed"] + : ["ChangeWeatherDisplaySimple"]), + "HideWeather", + "OpenLearnMoreURL", + ]; + + // Only return the widget if we have data. Otherwise, show error state + if (WEATHER_SUGGESTION) { + return ( + <div ref={this.setImpressionRef} className={outerClassName}> + <div className="weatherCard"> + <a + data-l10n-id="newtab-weather-see-forecast" + data-l10n-args='{"provider": "AccuWeather"}' + href={WEATHER_SUGGESTION.forecast.url} + className="weatherInfoLink" + onClick={this.onProviderClick} + > + <div className="weatherIconCol"> + <span + className={`weatherIcon iconId${WEATHER_SUGGESTION.current_conditions.icon_id}`} + /> + </div> + <div className="weatherText"> + <div className="weatherForecastRow"> + <span className="weatherTemperature"> + { + WEATHER_SUGGESTION.current_conditions.temperature[ + this.props.Prefs.values["weather.temperatureUnits"] + ] + } + °{this.props.Prefs.values["weather.temperatureUnits"]} + </span> + </div> + <div className="weatherCityRow"> + <span className="weatherCity"> + {WEATHER_SUGGESTION.city_name} + </span> + </div> + {showDetailedView ? ( + <div className="weatherDetailedSummaryRow"> + <div className="weatherHighLowTemps"> + {/* Low Forecasted Temperature */} + <span> + { + WEATHER_SUGGESTION.forecast.high[ + this.props.Prefs.values["weather.temperatureUnits"] + ] + } + ° + {this.props.Prefs.values["weather.temperatureUnits"]} + </span> + {/* Spacer / Bullet */} + <span>•</span> + {/* Low Forecasted Temperature */} + <span> + { + WEATHER_SUGGESTION.forecast.low[ + this.props.Prefs.values["weather.temperatureUnits"] + ] + } + ° + {this.props.Prefs.values["weather.temperatureUnits"]} + </span> + </div> + <span className="weatherTextSummary"> + {WEATHER_SUGGESTION.current_conditions.summary} + </span> + </div> + ) : null} + </div> + </a> + <div className="weatherButtonContextMenuWrapper"> + <button + aria-haspopup="true" + onKeyDown={this.onKeyDown} + onClick={this.onClick} + data-l10n-id="newtab-menu-section-tooltip" + className="weatherButtonContextMenu" + > + {showContextMenu ? ( + <LinkMenu + dispatch={dispatch} + index={index} + source={eventSource} + onUpdate={this.onUpdate} + options={WEATHER_SOURCE_CONTEXT_MENU_OPTIONS} + site={{ + url: "https://support.mozilla.org/kb/customize-items-on-firefox-new-tab-page", + }} + link="https://support.mozilla.org/kb/customize-items-on-firefox-new-tab-page" + shouldSendImpressionStats={shouldSendImpressionStats} + /> + ) : null} + </button> + </div> + </div> + <span + data-l10n-id="newtab-weather-sponsored" + data-l10n-args='{"provider": "AccuWeather"}' + className="weatherSponsorText" + ></span> + </div> + ); + } + + return ( + <div ref={this.setErrorRef} className={outerClassName}> + <div className="weatherNotAvailable"> + <span className="icon icon-small-spacer icon-info-critical" />{" "} + <span data-l10n-id="newtab-weather-error-not-available"></span> + </div> + </div> + ); + } +} + +export const Weather = connect(state => ({ + Weather: state.Weather, + Prefs: state.Prefs, + IntersectionObserver: globalThis.IntersectionObserver, + document: globalThis.document, +}))(_Weather); diff --git a/browser/components/newtab/content-src/components/Weather/_Weather.scss b/browser/components/newtab/content-src/components/Weather/_Weather.scss new file mode 100644 index 0000000000..0616530f98 --- /dev/null +++ b/browser/components/newtab/content-src/components/Weather/_Weather.scss @@ -0,0 +1,393 @@ +// Custom font sizing for weather widget +:root { + --newtab-weather-content-font-size: 11px; + --newtab-weather-sponsor-font-size: 8px; +} + +.weather { + font-size: var(--font-size-root); + position: absolute; + left: var(--space-xlarge); + top: var(--space-xlarge); + z-index: 1; +} + +// Unavailable / Error State +.weatherNotAvailable { + font-size: var(--newtab-weather-content-font-size); + color: var(--text-color-error); + display: flex; + align-items: center; + + .icon { + fill: var(--icon-color-critical); + -moz-context-properties: fill; + } +} + +.weatherCard { + margin-block-end: var(--space-xsmall); + display: flex; + flex-wrap: nowrap; + align-items: stretch; + border-radius: var(--border-radius-medium); + overflow: hidden; + + &:hover, &:focus-within { + ~ .weatherSponsorText { + visibility: visible; + } + } + + &:focus-within { + overflow: visible; + } + + &:hover { + box-shadow: var(--box-shadow-10); + background: var(--background-color-box); + } + + a { + color: var(--text-color); + } + +} + +.weatherSponsorText { + visibility: hidden; + font-size: var(--newtab-weather-sponsor-font-size); + color: var(--text-color-deemphasized); +} + +.weatherInfoLink, .weatherButtonContextMenuWrapper { + appearance: none; + background-color: var(--background-color-ghost); + border: 0; + padding: var(--space-small); + cursor: pointer; + + &:hover { + // TODO: Add Wallpaper Background Color Fix + background-color: var(--button-background-color-ghost-hover); + + &::after { + background-color: transparent + } + + &:active { + // TODO: Add Wallpaper Background Color Fix + background-color: var(--button-background-color-ghost-active); + } + } + + &:focus-visible { + outline: var(--focus-outline); + } + + // Contrast fix for users who have wallpapers set + .hasWallpaperDark & { + @media (prefers-color-scheme: dark) { + // TODO: Replace with token + background-color: rgba(35, 34, 43, 70%); + + &:hover { + background-color: var(--newtab-button-static-hover-background); + } + + &:hover:active { + background-color: var(--newtab-button-static-active-background); + } + } + + @media (prefers-contrast) and (prefers-color-scheme: dark) { + background-color: var(--background-color-box); + } + } + + .hasWallpaperLight & { + @media (prefers-color-scheme: light) { + // TODO: Replace with token + background-color: rgba(255, 255, 255, 70%); + + &:hover { + background-color: var(--newtab-button-static-hover-background); + } + + &:hover:active { + background-color: var(--newtab-button-static-active-background); + } + } + + @media (prefers-contrast) and (prefers-color-scheme: light) { + background-color: var(--background-color-box); + } + } + +} + +.weatherInfoLink { + display: flex; + gap: var(--space-medium); + padding: var(--space-small) var(--space-medium); + border-radius: var(--border-radius-medium) 0 0 var(--border-radius-medium); + text-decoration: none; + color: var(--text-color);; + min-width: 130px; + max-width: 190px; + text-overflow: ellipsis; + + @media(min-width: $break-point-medium) { + min-width: unset; + } + + &:hover ~.weatherButtonContextMenuWrapper { + &::after { + background-color: transparent + } + } + + &:focus-visible { + border-radius: var(--border-radius-medium); + + ~ .weatherButtonContextMenuWrapper { + &::after { + background-color: transparent + } + } + } +} + +.weatherButtonContextMenuWrapper { + position: relative; + cursor: pointer; + border-radius: 0 var(--border-radius-medium) var(--border-radius-medium) 0; + display: flex; + align-items: stretch; + width: 50px; + padding: 0; + + &::after { + content: ''; + left: 0; + top: 10px; + height: calc(100% - 20px); + width: 1px; + background-color: var(--newtab-button-static-background); + display: block; + position: absolute; + z-index: 0; + } + + @media (prefers-color-scheme: dark) { + &::after { + background-color: var(--color-gray-70); + } + } + + &:hover { + &::after { + background-color: transparent + } + } + + &:focus-visible { + border-radius: var(--border-radius-medium); + + &::after { + background-color: transparent + } + } +} + +.weatherButtonContextMenu { + background-image: url('chrome://global/skin/icons/more.svg'); + background-repeat: no-repeat; + background-size: var(--size-item-small) auto; + background-position: center; + background-color: transparent; + cursor: pointer; + fill: var(--icon-color); + -moz-context-properties: fill; + width: 100%; + height: 100%; + border: 0; + appearance: none; + min-width: var(--size-item-large); +} + +.weatherText { + height: min-content; +} + +.weatherCityRow, .weatherForecastRow, .weatherDetailedSummaryRow { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-small); +} + +.weatherForecastRow { + text-transform: uppercase; + font-weight: var(--font-weight-bold); +} + +.weatherCityRow { + color: var(--text-color-deemphasized); +} + +.weatherCity { + text-overflow: ellipsis; + font-size: var(--font-size-small); +} + +// Add additional margin if detailed summary is in view +.weatherCityRow + .weatherDetailedSummaryRow { + margin-block-start: var(--space-xsmall); +} + +.weatherDetailedSummaryRow { + font-size: var(--newtab-weather-content-font-size); + gap: var(--space-large); +} + +.weatherHighLowTemps { + display: flex; + gap: var(--space-xxsmall); + text-transform: uppercase; + word-spacing: var(--space-xxsmall); +} + +.weatherTextSummary { + text-align: center; + max-width: 90px; +} + +.weatherTemperature { + font-size: var(--font-size-large); +} + +// Weather Symbol Icons +.weatherIconCol { + width: var(--size-item-large); + height: var(--size-item-large); + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + align-self: center; +} + +.weatherIcon { + width: var(--size-item-large); + height: auto; + vertical-align: middle; + + @media (prefers-contrast) { + -moz-context-properties: fill, stroke; + fill: currentColor; + stroke: currentColor; + } + + &.iconId1 { + content: url('chrome://browser/skin/weather/sunny.svg'); + // height: var(--size-item-large); + } + + &.iconId2 { + content: url('chrome://browser/skin/weather/mostly-sunny.svg'); + // height: var(--size-item-large); + } + + &:is(.iconId3, .iconId4, .iconId6) { + content: url('chrome://browser/skin/weather/partly-sunny.svg'); + // height: var(--size-item-large); + } + + &.iconId5 { + content: url('chrome://browser/skin/weather/hazy-sunshine.svg'); + // height: var(--size-item-large); + } + + &:is(.iconId7, .iconId8) { + content: url('chrome://browser/skin/weather/cloudy.svg'); + } + + &.iconId11 { + content: url('chrome://browser/skin/weather/fog.svg'); + } + + &.iconId12 { + content: url('chrome://browser/skin/weather/showers.svg'); + } + + &:is(.iconId13, .iconId14) { + content: url('chrome://browser/skin/weather/mostly-cloudy-with-showers.svg'); + // height: var(--size-item-large); + } + + &.iconId15 { + content: url('chrome://browser/skin/weather/thunderstorms.svg'); + } + + &:is(.iconId16, .iconId17) { + content: url('chrome://browser/skin/weather/mostly-cloudy-with-thunderstorms.svg'); + } + + &.iconId18 { + content: url('chrome://browser/skin/weather/rain.svg'); + } + + &:is(.iconId19, .iconId20, .iconId25) { + content: url('chrome://browser/skin/weather/flurries.svg'); + } + + &.iconId21 { + content: url('chrome://browser/skin/weather/partly-sunny-with-flurries.svg'); + } + + &:is(.iconId22, .iconId23) { + content: url('chrome://browser/skin/weather/snow.svg'); + } + + &:is(.iconId24, .iconId31) { + content: url('chrome://browser/skin/weather/ice.svg'); + } + + &:is(.iconId26, .iconId29) { + content: url('chrome://browser/skin/weather/freezing-rain.svg'); + } + + &.iconId30 { + content: url('chrome://browser/skin/weather/hot.svg'); + } + + &.iconId32 { + content: url('chrome://browser/skin/weather/windy.svg'); + } + + &.iconId33 { + content: url('chrome://browser/skin/weather/night-clear.svg'); + } + + &:is(.iconId34, .iconId35, .iconId36, .iconId38) { + content: url('chrome://browser/skin/weather/night-mostly-clear.svg'); + } + + &.iconId37 { + content: url('chrome://browser/skin/weather/night-hazy-moonlight.svg'); + } + + &:is(.iconId39, .iconId40) { + content: url('chrome://browser/skin/weather/night-partly-cloudy-with-showers.svg'); + height: var(--size-item-large); + } + + &:is(.iconId41, .iconId42) { + content: url('chrome://browser/skin/weather/night-partly-cloudy-with-thunderstorms.svg'); + } + + &:is(.iconId43, .iconId44) { + content: url('chrome://browser/skin/weather/night-mostly-cloudy-with-flurries.svg'); + } +} diff --git a/browser/components/newtab/content-src/lib/link-menu-options.mjs b/browser/components/newtab/content-src/lib/link-menu-options.mjs index f10a5e34c6..23dcf8b050 100644 --- a/browser/components/newtab/content-src/lib/link-menu-options.mjs +++ b/browser/components/newtab/content-src/lib/link-menu-options.mjs @@ -306,4 +306,68 @@ export const LinkMenuOptions = { : LinkMenuOptions.EmptyItem(), OpenInPrivateWindow: (site, index, eventSource, isEnabled) => isEnabled ? _OpenInPrivateWindow(site) : LinkMenuOptions.EmptyItem(), + ChangeWeatherLocation: () => ({ + id: "newtab-weather-menu-change-location", + action: ac.OnlyToMain({ + type: at.CHANGE_WEATHER_LOCATION, + data: { url: "https://mozilla.org" }, + }), + }), + ChangeWeatherDisplaySimple: () => ({ + id: "newtab-weather-menu-change-weather-display-simple", + action: ac.OnlyToMain({ + type: at.SET_PREF, + data: { + name: "weather.display", + value: "simple", + }, + }), + }), + ChangeWeatherDisplayDetailed: () => ({ + id: "newtab-weather-menu-change-weather-display-detailed", + action: ac.OnlyToMain({ + type: at.SET_PREF, + data: { + name: "weather.display", + value: "detailed", + }, + }), + }), + ChangeTempUnitFahrenheit: () => ({ + id: "newtab-weather-menu-change-temperature-units-fahrenheit", + action: ac.OnlyToMain({ + type: at.SET_PREF, + data: { + name: "weather.temperatureUnits", + value: "f", + }, + }), + }), + ChangeTempUnitCelsius: () => ({ + id: "newtab-weather-menu-change-temperature-units-celsius", + action: ac.OnlyToMain({ + type: at.SET_PREF, + data: { + name: "weather.temperatureUnits", + value: "c", + }, + }), + }), + HideWeather: () => ({ + id: "newtab-weather-menu-hide-weather", + action: ac.OnlyToMain({ + type: at.SET_PREF, + data: { + name: "showWeather", + value: false, + }, + }), + }), + OpenLearnMoreURL: site => ({ + id: "newtab-weather-menu-learn-more", + action: ac.OnlyToMain({ + type: at.OPEN_LINK, + data: { url: site.url }, + }), + }), }; diff --git a/browser/components/newtab/content-src/styles/_activity-stream.scss b/browser/components/newtab/content-src/styles/_activity-stream.scss index d2e66667b2..580f35416e 100644 --- a/browser/components/newtab/content-src/styles/_activity-stream.scss +++ b/browser/components/newtab/content-src/styles/_activity-stream.scss @@ -149,6 +149,7 @@ input { @import '../components/ConfirmDialog/ConfirmDialog'; @import '../components/CustomizeMenu/CustomizeMenu'; @import '../components/WallpapersSection/WallpapersSection'; +@import '../components/Weather/Weather'; @import '../components/Card/Card'; @import '../components/CollapsibleSection/CollapsibleSection'; @import '../components/DiscoveryStreamAdmin/DiscoveryStreamAdmin'; diff --git a/browser/components/newtab/content-src/styles/_icons.scss b/browser/components/newtab/content-src/styles/_icons.scss index 8be97ad9ae..39879b2b44 100644 --- a/browser/components/newtab/content-src/styles/_icons.scss +++ b/browser/components/newtab/content-src/styles/_icons.scss @@ -4,7 +4,7 @@ background-size: $icon-size; -moz-context-properties: fill; display: inline-block; - color: var(--newtab-text-primary-color); + color: var(--icon-color); fill: currentColor; height: $icon-size; vertical-align: middle; @@ -70,6 +70,10 @@ background-image: url('chrome://global/skin/icons/info.svg'); } + &.icon-info-critical { + background-image: url('chrome://activity-stream/content/data/content/assets/glyph-info-critical-16.svg'); + } + &.icon-help { background-image: url('chrome://global/skin/icons/help.svg'); } @@ -167,6 +171,10 @@ background-image: url('chrome://activity-stream/content/data/content/assets/glyph-webextension-16.svg'); } + &.icon-weather { + background-image: url('chrome://browser/skin/weather/sunny.svg'); + } + &.icon-highlights { background-image: url('chrome://global/skin/icons/highlights.svg'); } diff --git a/browser/components/newtab/content-src/styles/_theme.scss b/browser/components/newtab/content-src/styles/_theme.scss index 6b097ae93e..78b54f4f8e 100644 --- a/browser/components/newtab/content-src/styles/_theme.scss +++ b/browser/components/newtab/content-src/styles/_theme.scss @@ -38,6 +38,21 @@ $shadow-image-inset: inset 0 0 0 0.5px $black-15; --newtab-element-hover-color: color-mix(in srgb, var(--newtab-background-color) 90%, #{$black}); --newtab-element-active-color: color-mix(in srgb, var(--newtab-background-color) 80%, #{$black}); + // --newtab-button-*-color is used on all new page card/top site options buttons + --newtab-button-background: var(--button-background-color); + --newtab-button-focus-background: var(--newtab-button-background); + --newtab-button-focus-border: var(--focus-outline-color); + --newtab-button-hover-background: var(--button-background-color-hover); + --newtab-button-active-background: var(--button-background-color-active); + --newtab-button-text: var(--button-text-color); + + // --newtab-button-static*-color is used on pocket cards and require a + // static color unit due to transparency issues with `color-mix` + --newtab-button-static-background: #F0F0F4; + --newtab-button-static-focus-background: var(--newtab-button-static-background); + --newtab-button-static-hover-background: #E0E0E6; + --newtab-button-static-active-background: #CFCFD8; + // --newtab-element-secondary*-color is used when an element needs to be set // off from the secondary background color. --newtab-element-secondary-color: color-mix(in srgb, currentColor 5%, transparent); @@ -87,6 +102,12 @@ $shadow-image-inset: inset 0 0 0 0.5px $black-15; --newtab-primary-element-text-color: #{$primary-text-color-dark}; --newtab-wordmark-color: #{$newtab-wordmark-darktheme-color}; --newtab-status-success: #{$status-dark-green}; + + // --newtab-button-static*-color is used on pocket cards and require a + // static color unit due to transparency issues with `color-mix` + --newtab-button-static-background: #2B2A33; + --newtab-button-static-hover-background: #52525E; + --newtab-button-static-active-background: #5B5B66; } } diff --git a/browser/components/newtab/content-src/styles/_variables.scss b/browser/components/newtab/content-src/styles/_variables.scss index 9fd0083841..43672c7796 100644 --- a/browser/components/newtab/content-src/styles/_variables.scss +++ b/browser/components/newtab/content-src/styles/_variables.scss @@ -157,14 +157,17 @@ $customize-menu-border-tint: 1px solid rgba(0, 0, 0, 15%); @mixin context-menu-button { .context-menu-button { background-clip: padding-box; - background-color: var(--newtab-background-color-secondary); + background-color: var(--newtab-button-background); background-image: url('chrome://global/skin/icons/more.svg'); background-position: 55%; - border: $border-primary; + border: 0; + outline: $border-primary; + outline-width: 0; border-radius: 100%; box-shadow: $context-menu-button-boxshadow; cursor: pointer; - fill: var(--newtab-text-primary-color); + color: var(--button-text-color); + fill: var(--newtab-button-text); height: $context-menu-button-size; inset-inline-end: math.div(-$context-menu-button-size, 2); opacity: 0; @@ -175,10 +178,26 @@ $customize-menu-border-tint: 1px solid rgba(0, 0, 0, 15%); transition-property: transform, opacity; width: $context-menu-button-size; - &:is(:active, :focus) { + &:is(:active, :focus-visible, :hover) { opacity: 1; transform: scale(1); } + + &:is(:hover) { + background-color: var(--newtab-button-hover-background); + } + + &:is(:focus-visible) { + outline-color: var(--newtab-button-focus-border); + background-color: var(--newtab-button-focus-background); + outline-width: 4px; + } + + &:is(:active) { + background-color: var(--newtab-button-active-background); + } + + } } diff --git a/browser/components/newtab/css/activity-stream-linux.css b/browser/components/newtab/css/activity-stream-linux.css index 131ffac535..248de6cf21 100644 --- a/browser/components/newtab/css/activity-stream-linux.css +++ b/browser/components/newtab/css/activity-stream-linux.css @@ -42,6 +42,16 @@ input { --newtab-text-secondary-color: color-mix(in srgb, var(--newtab-text-primary-color) 70%, transparent); --newtab-element-hover-color: color-mix(in srgb, var(--newtab-background-color) 90%, #000); --newtab-element-active-color: color-mix(in srgb, var(--newtab-background-color) 80%, #000); + --newtab-button-background: var(--button-background-color); + --newtab-button-focus-background: var(--newtab-button-background); + --newtab-button-focus-border: var(--focus-outline-color); + --newtab-button-hover-background: var(--button-background-color-hover); + --newtab-button-active-background: var(--button-background-color-active); + --newtab-button-text: var(--button-text-color); + --newtab-button-static-background: #F0F0F4; + --newtab-button-static-focus-background: var(--newtab-button-static-background); + --newtab-button-static-hover-background: #E0E0E6; + --newtab-button-static-active-background: #CFCFD8; --newtab-element-secondary-color: color-mix(in srgb, currentColor 5%, transparent); --newtab-element-secondary-hover-color: color-mix(in srgb, currentColor 12%, transparent); --newtab-element-secondary-active-color: color-mix(in srgb, currentColor 25%, transparent); @@ -79,6 +89,9 @@ input { --newtab-primary-element-text-color: #2b2a33; --newtab-wordmark-color: #fbfbfe; --newtab-status-success: #7C6; + --newtab-button-static-background: #2B2A33; + --newtab-button-static-hover-background: #52525E; + --newtab-button-static-active-background: #5B5B66; } @media (prefers-contrast) { @@ -92,7 +105,7 @@ input { background-size: 16px; -moz-context-properties: fill; display: inline-block; - color: var(--newtab-text-primary-color); + color: var(--icon-color); fill: currentColor; height: 16px; vertical-align: middle; @@ -142,6 +155,9 @@ input { .icon.icon-info { background-image: url("chrome://global/skin/icons/info.svg"); } +.icon.icon-info-critical { + background-image: url("chrome://activity-stream/content/data/content/assets/glyph-info-critical-16.svg"); +} .icon.icon-help { background-image: url("chrome://global/skin/icons/help.svg"); } @@ -222,6 +238,9 @@ input { .icon.icon-webextension { background-image: url("chrome://activity-stream/content/data/content/assets/glyph-webextension-16.svg"); } +.icon.icon-weather { + background-image: url("chrome://browser/skin/weather/sunny.svg"); +} .icon.icon-highlights { background-image: url("chrome://global/skin/icons/highlights.svg"); } @@ -659,12 +678,17 @@ main section { .top-site-outer:is(:hover) .context-menu-button { opacity: 1; } +.top-site-outer.active .context-menu-button { + opacity: 1; + background-color: var(--newtab-button-active-background); +} .top-site-outer .context-menu-button { background-image: url("chrome://global/skin/icons/more.svg"); + background-color: var(--newtab-button-background); border: 0; border-radius: 4px; cursor: pointer; - fill: var(--newtab-text-primary-color); + fill: var(--newtab-button-text); -moz-context-properties: fill; height: 20px; width: 20px; @@ -674,11 +698,16 @@ main section { top: -20px; transition: opacity 200ms; } -.top-site-outer .context-menu-button:is(:active, :focus) { - outline: 0; +.top-site-outer .context-menu-button:hover { + background-color: var(--newtab-button-hover-background); +} +.top-site-outer .context-menu-button:hover:active { + background-color: var(--newtab-button-active-background); +} +.top-site-outer .context-menu-button:focus-visible { + background-color: var(--newtab-button-focus-background); + border-color: var(--newtab-button-focus-border); opacity: 1; - background-color: var(--newtab-element-hover-color); - fill: var(--newtab-primary-action-background); } .top-site-outer .tile { border-radius: 8px; @@ -1675,7 +1704,8 @@ main section { inset-block: 0; inset-inline-end: 0; z-index: 1001; - padding: 16px; + padding-block: 0 var(--space-large); + padding-inline: var(--space-large); overflow: auto; transform: translateX(435px); visibility: hidden; @@ -1702,9 +1732,16 @@ main section { .customize-menu.customize-animate-exit-active { box-shadow: 0 2px 14px 0 rgba(0, 0, 0, 0.2); } +.customize-menu .close-button-wrapper { + position: sticky; + top: 0; + padding-block-start: var(--space-large); + background-color: var(--newtab-background-color-secondary); + z-index: 1; +} .customize-menu .close-button { margin-inline-start: auto; - margin-bottom: 28px; + margin-inline-end: var(--space-large); white-space: nowrap; display: block; background-color: var(--newtab-element-secondary-color); @@ -1731,7 +1768,7 @@ main section { grid-template-columns: 1fr; grid-template-rows: repeat(4, auto); grid-row-gap: 32px; - padding: 0 16px; + padding: var(--space-large); } .home-section .wallpapers-section h2 { font-size: inherit; @@ -1978,6 +2015,333 @@ main section { text-decoration: none; } +:root { + --newtab-weather-content-font-size: 11px; + --newtab-weather-sponsor-font-size: 8px; +} + +.weather { + font-size: var(--font-size-root); + position: absolute; + left: var(--space-xlarge); + top: var(--space-xlarge); + z-index: 1; +} + +.weatherNotAvailable { + font-size: var(--newtab-weather-content-font-size); + color: var(--text-color-error); + display: flex; + align-items: center; +} +.weatherNotAvailable .icon { + fill: var(--icon-color-critical); + -moz-context-properties: fill; +} + +.weatherCard { + margin-block-end: var(--space-xsmall); + display: flex; + flex-wrap: nowrap; + align-items: stretch; + border-radius: var(--border-radius-medium); + overflow: hidden; +} +.weatherCard:hover ~ .weatherSponsorText, .weatherCard:focus-within ~ .weatherSponsorText { + visibility: visible; +} +.weatherCard:focus-within { + overflow: visible; +} +.weatherCard:hover { + box-shadow: var(--box-shadow-10); + background: var(--background-color-box); +} +.weatherCard a { + color: var(--text-color); +} + +.weatherSponsorText { + visibility: hidden; + font-size: var(--newtab-weather-sponsor-font-size); + color: var(--text-color-deemphasized); +} + +.weatherInfoLink, .weatherButtonContextMenuWrapper { + appearance: none; + background-color: var(--background-color-ghost); + border: 0; + padding: var(--space-small); + cursor: pointer; +} +.weatherInfoLink:hover, .weatherButtonContextMenuWrapper:hover { + background-color: var(--button-background-color-ghost-hover); +} +.weatherInfoLink:hover::after, .weatherButtonContextMenuWrapper:hover::after { + background-color: transparent; +} +.weatherInfoLink:hover:active, .weatherButtonContextMenuWrapper:hover:active { + background-color: var(--button-background-color-ghost-active); +} +.weatherInfoLink:focus-visible, .weatherButtonContextMenuWrapper:focus-visible { + outline: var(--focus-outline); +} +@media (prefers-color-scheme: dark) { + .hasWallpaperDark .weatherInfoLink, .hasWallpaperDark .weatherButtonContextMenuWrapper { + background-color: rgba(35, 34, 43, 0.7); + } + .hasWallpaperDark .weatherInfoLink:hover, .hasWallpaperDark .weatherButtonContextMenuWrapper:hover { + background-color: var(--newtab-button-static-hover-background); + } + .hasWallpaperDark .weatherInfoLink:hover:active, .hasWallpaperDark .weatherButtonContextMenuWrapper:hover:active { + background-color: var(--newtab-button-static-active-background); + } +} +@media (prefers-contrast) and (prefers-color-scheme: dark) { + .hasWallpaperDark .weatherInfoLink, .hasWallpaperDark .weatherButtonContextMenuWrapper { + background-color: var(--background-color-box); + } +} +@media (prefers-color-scheme: light) { + .hasWallpaperLight .weatherInfoLink, .hasWallpaperLight .weatherButtonContextMenuWrapper { + background-color: rgba(255, 255, 255, 0.7); + } + .hasWallpaperLight .weatherInfoLink:hover, .hasWallpaperLight .weatherButtonContextMenuWrapper:hover { + background-color: var(--newtab-button-static-hover-background); + } + .hasWallpaperLight .weatherInfoLink:hover:active, .hasWallpaperLight .weatherButtonContextMenuWrapper:hover:active { + background-color: var(--newtab-button-static-active-background); + } +} +@media (prefers-contrast) and (prefers-color-scheme: light) { + .hasWallpaperLight .weatherInfoLink, .hasWallpaperLight .weatherButtonContextMenuWrapper { + background-color: var(--background-color-box); + } +} + +.weatherInfoLink { + display: flex; + gap: var(--space-medium); + padding: var(--space-small) var(--space-medium); + border-radius: var(--border-radius-medium) 0 0 var(--border-radius-medium); + text-decoration: none; + color: var(--text-color); + min-width: 130px; + max-width: 190px; + text-overflow: ellipsis; +} +@media (min-width: 610px) { + .weatherInfoLink { + min-width: unset; + } +} +.weatherInfoLink:hover ~ .weatherButtonContextMenuWrapper::after { + background-color: transparent; +} +.weatherInfoLink:focus-visible { + border-radius: var(--border-radius-medium); +} +.weatherInfoLink:focus-visible ~ .weatherButtonContextMenuWrapper::after { + background-color: transparent; +} + +.weatherButtonContextMenuWrapper { + position: relative; + cursor: pointer; + border-radius: 0 var(--border-radius-medium) var(--border-radius-medium) 0; + display: flex; + align-items: stretch; + width: 50px; + padding: 0; +} +.weatherButtonContextMenuWrapper::after { + content: ""; + left: 0; + top: 10px; + height: calc(100% - 20px); + width: 1px; + background-color: var(--newtab-button-static-background); + display: block; + position: absolute; + z-index: 0; +} +@media (prefers-color-scheme: dark) { + .weatherButtonContextMenuWrapper::after { + background-color: var(--color-gray-70); + } +} +.weatherButtonContextMenuWrapper:hover::after { + background-color: transparent; +} +.weatherButtonContextMenuWrapper:focus-visible { + border-radius: var(--border-radius-medium); +} +.weatherButtonContextMenuWrapper:focus-visible::after { + background-color: transparent; +} + +.weatherButtonContextMenu { + background-image: url("chrome://global/skin/icons/more.svg"); + background-repeat: no-repeat; + background-size: var(--size-item-small) auto; + background-position: center; + background-color: transparent; + cursor: pointer; + fill: var(--icon-color); + -moz-context-properties: fill; + width: 100%; + height: 100%; + border: 0; + appearance: none; + min-width: var(--size-item-large); +} + +.weatherText { + height: min-content; +} + +.weatherCityRow, .weatherForecastRow, .weatherDetailedSummaryRow { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-small); +} + +.weatherForecastRow { + text-transform: uppercase; + font-weight: var(--font-weight-bold); +} + +.weatherCityRow { + color: var(--text-color-deemphasized); +} + +.weatherCity { + text-overflow: ellipsis; + font-size: var(--font-size-small); +} + +.weatherCityRow + .weatherDetailedSummaryRow { + margin-block-start: var(--space-xsmall); +} + +.weatherDetailedSummaryRow { + font-size: var(--newtab-weather-content-font-size); + gap: var(--space-large); +} + +.weatherHighLowTemps { + display: flex; + gap: var(--space-xxsmall); + text-transform: uppercase; + word-spacing: var(--space-xxsmall); +} + +.weatherTextSummary { + text-align: center; + max-width: 90px; +} + +.weatherTemperature { + font-size: var(--font-size-large); +} + +.weatherIconCol { + width: var(--size-item-large); + height: var(--size-item-large); + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + align-self: center; +} + +.weatherIcon { + width: var(--size-item-large); + height: auto; + vertical-align: middle; +} +@media (prefers-contrast) { + .weatherIcon { + -moz-context-properties: fill, stroke; + fill: currentColor; + stroke: currentColor; + } +} +.weatherIcon.iconId1 { + content: url("chrome://browser/skin/weather/sunny.svg"); +} +.weatherIcon.iconId2 { + content: url("chrome://browser/skin/weather/mostly-sunny.svg"); +} +.weatherIcon:is(.iconId3, .iconId4, .iconId6) { + content: url("chrome://browser/skin/weather/partly-sunny.svg"); +} +.weatherIcon.iconId5 { + content: url("chrome://browser/skin/weather/hazy-sunshine.svg"); +} +.weatherIcon:is(.iconId7, .iconId8) { + content: url("chrome://browser/skin/weather/cloudy.svg"); +} +.weatherIcon.iconId11 { + content: url("chrome://browser/skin/weather/fog.svg"); +} +.weatherIcon.iconId12 { + content: url("chrome://browser/skin/weather/showers.svg"); +} +.weatherIcon:is(.iconId13, .iconId14) { + content: url("chrome://browser/skin/weather/mostly-cloudy-with-showers.svg"); +} +.weatherIcon.iconId15 { + content: url("chrome://browser/skin/weather/thunderstorms.svg"); +} +.weatherIcon:is(.iconId16, .iconId17) { + content: url("chrome://browser/skin/weather/mostly-cloudy-with-thunderstorms.svg"); +} +.weatherIcon.iconId18 { + content: url("chrome://browser/skin/weather/rain.svg"); +} +.weatherIcon:is(.iconId19, .iconId20, .iconId25) { + content: url("chrome://browser/skin/weather/flurries.svg"); +} +.weatherIcon.iconId21 { + content: url("chrome://browser/skin/weather/partly-sunny-with-flurries.svg"); +} +.weatherIcon:is(.iconId22, .iconId23) { + content: url("chrome://browser/skin/weather/snow.svg"); +} +.weatherIcon:is(.iconId24, .iconId31) { + content: url("chrome://browser/skin/weather/ice.svg"); +} +.weatherIcon:is(.iconId26, .iconId29) { + content: url("chrome://browser/skin/weather/freezing-rain.svg"); +} +.weatherIcon.iconId30 { + content: url("chrome://browser/skin/weather/hot.svg"); +} +.weatherIcon.iconId32 { + content: url("chrome://browser/skin/weather/windy.svg"); +} +.weatherIcon.iconId33 { + content: url("chrome://browser/skin/weather/night-clear.svg"); +} +.weatherIcon:is(.iconId34, .iconId35, .iconId36, .iconId38) { + content: url("chrome://browser/skin/weather/night-mostly-clear.svg"); +} +.weatherIcon.iconId37 { + content: url("chrome://browser/skin/weather/night-hazy-moonlight.svg"); +} +.weatherIcon:is(.iconId39, .iconId40) { + content: url("chrome://browser/skin/weather/night-partly-cloudy-with-showers.svg"); + height: var(--size-item-large); +} +.weatherIcon:is(.iconId41, .iconId42) { + content: url("chrome://browser/skin/weather/night-partly-cloudy-with-thunderstorms.svg"); +} +.weatherIcon:is(.iconId43, .iconId44) { + content: url("chrome://browser/skin/weather/night-mostly-cloudy-with-flurries.svg"); +} + /* stylelint-disable max-nesting-depth */ .card-outer { background: var(--newtab-background-color-secondary); @@ -1990,14 +2354,17 @@ main section { } .card-outer .context-menu-button { background-clip: padding-box; - background-color: var(--newtab-background-color-secondary); + background-color: var(--newtab-button-background); background-image: url("chrome://global/skin/icons/more.svg"); background-position: 55%; - border: 1px solid var(--newtab-border-color); + border: 0; + outline: 1px solid var(--newtab-border-color); + outline-width: 0; border-radius: 100%; box-shadow: 0 2px rgba(12, 12, 13, 0.1); cursor: pointer; - fill: var(--newtab-text-primary-color); + color: var(--button-text-color); + fill: var(--newtab-button-text); height: 27px; inset-inline-end: -13.5px; opacity: 0; @@ -2008,10 +2375,21 @@ main section { transition-property: transform, opacity; width: 27px; } -.card-outer .context-menu-button:is(:active, :focus) { +.card-outer .context-menu-button:is(:active, :focus-visible, :hover) { opacity: 1; transform: scale(1); } +.card-outer .context-menu-button:is(:hover) { + background-color: var(--newtab-button-hover-background); +} +.card-outer .context-menu-button:is(:focus-visible) { + outline-color: var(--newtab-button-focus-border); + background-color: var(--newtab-button-focus-background); + outline-width: 4px; +} +.card-outer .context-menu-button:is(:active) { + background-color: var(--newtab-button-active-background); +} .card-outer:is(:focus):not(.placeholder) { border: 0; outline: 0; @@ -2618,6 +2996,15 @@ main section { width: auto; flex-grow: 1; } +.discoverystream-admin .weather-section { + margin-block-end: 24px; +} +.discoverystream-admin .weather-section form { + display: flex; +} +.discoverystream-admin .weather-section form label { + margin-inline-end: 12px; +} .pocket-logged-in-cta { font-size: 13px; @@ -3305,6 +3692,18 @@ main section { .ds-highlights .section .section-list .card-outer a { text-decoration: none; } +.ds-highlights .section .section-list .card-outer .context-menu-button { + background-color: var(--newtab-button-static-background); +} +.ds-highlights .section .section-list .card-outer .context-menu-button:hover { + background-color: var(--newtab-button-static-hover-background); +} +.ds-highlights .section .section-list .card-outer .context-menu-button:hover:active { + background-color: var(--newtab-button-static-active-background); +} +.ds-highlights .section .section-list .card-outer .context-menu-button:focus-visible { + background-color: var(--newtab-button-static-focus-background); +} .ds-highlights .hide-for-narrow { display: block; } @@ -3566,14 +3965,17 @@ main section { .ds-card .context-menu-button, .ds-signup .context-menu-button { background-clip: padding-box; - background-color: var(--newtab-background-color-secondary); + background-color: var(--newtab-button-background); background-image: url("chrome://global/skin/icons/more.svg"); background-position: 55%; - border: 1px solid var(--newtab-border-color); + border: 0; + outline: 1px solid var(--newtab-border-color); + outline-width: 0; border-radius: 100%; box-shadow: 0 2px rgba(12, 12, 13, 0.1); cursor: pointer; - fill: var(--newtab-text-primary-color); + color: var(--button-text-color); + fill: var(--newtab-button-text); height: 27px; inset-inline-end: -13.5px; opacity: 0; @@ -3584,11 +3986,25 @@ main section { transition-property: transform, opacity; width: 27px; } -.ds-card .context-menu-button:is(:active, :focus), -.ds-signup .context-menu-button:is(:active, :focus) { +.ds-card .context-menu-button:is(:active, :focus-visible, :hover), +.ds-signup .context-menu-button:is(:active, :focus-visible, :hover) { opacity: 1; transform: scale(1); } +.ds-card .context-menu-button:is(:hover), +.ds-signup .context-menu-button:is(:hover) { + background-color: var(--newtab-button-hover-background); +} +.ds-card .context-menu-button:is(:focus-visible), +.ds-signup .context-menu-button:is(:focus-visible) { + outline-color: var(--newtab-button-focus-border); + background-color: var(--newtab-button-focus-background); + outline-width: 4px; +} +.ds-card .context-menu-button:is(:active), +.ds-signup .context-menu-button:is(:active) { + background-color: var(--newtab-button-active-background); +} .ds-card .context-menu, .ds-signup .context-menu { opacity: 0; @@ -3663,11 +4079,6 @@ main section { background-size: 15px; fill: #FFF; } -.ds-card .card-stp-button-hover-background .context-menu-button { - position: static; - transition: none; - border-radius: 3px; -} .ds-card .card-stp-button-hover-background .context-menu-position-container { position: relative; } @@ -3690,6 +4101,9 @@ main section { white-space: nowrap; color: #FFF; } +.ds-card .card-stp-button-hover-background .card-stp-button:focus-visible { + outline: 2px solid var(--newtab-button-focus-border); +} .ds-card .card-stp-button-hover-background button, .ds-card .card-stp-button-hover-background .context-menu { pointer-events: auto; @@ -3697,6 +4111,22 @@ main section { .ds-card .card-stp-button-hover-background button { cursor: pointer; } +.ds-card .context-menu-button { + position: static; + transition: none; + border-radius: 3px; + background-color: var(--newtab-button-static-background); +} +.ds-card .context-menu-button:hover { + background-color: var(--newtab-button-static-hover-background); +} +.ds-card .context-menu-button:hover:active { + background-color: var(--newtab-button-static-active-background); +} +.ds-card .context-menu-button:focus-visible { + outline: 2px solid var(--newtab-button-focus-border); + background-color: var(--newtab-button-static-focus-background); +} .ds-card.last-item .card-stp-button-hover-background .context-menu { margin-inline-start: auto; margin-inline-end: 18.5px; diff --git a/browser/components/newtab/css/activity-stream-mac.css b/browser/components/newtab/css/activity-stream-mac.css index 416209d511..7b5ef57cb5 100644 --- a/browser/components/newtab/css/activity-stream-mac.css +++ b/browser/components/newtab/css/activity-stream-mac.css @@ -46,6 +46,16 @@ input { --newtab-text-secondary-color: color-mix(in srgb, var(--newtab-text-primary-color) 70%, transparent); --newtab-element-hover-color: color-mix(in srgb, var(--newtab-background-color) 90%, #000); --newtab-element-active-color: color-mix(in srgb, var(--newtab-background-color) 80%, #000); + --newtab-button-background: var(--button-background-color); + --newtab-button-focus-background: var(--newtab-button-background); + --newtab-button-focus-border: var(--focus-outline-color); + --newtab-button-hover-background: var(--button-background-color-hover); + --newtab-button-active-background: var(--button-background-color-active); + --newtab-button-text: var(--button-text-color); + --newtab-button-static-background: #F0F0F4; + --newtab-button-static-focus-background: var(--newtab-button-static-background); + --newtab-button-static-hover-background: #E0E0E6; + --newtab-button-static-active-background: #CFCFD8; --newtab-element-secondary-color: color-mix(in srgb, currentColor 5%, transparent); --newtab-element-secondary-hover-color: color-mix(in srgb, currentColor 12%, transparent); --newtab-element-secondary-active-color: color-mix(in srgb, currentColor 25%, transparent); @@ -83,6 +93,9 @@ input { --newtab-primary-element-text-color: #2b2a33; --newtab-wordmark-color: #fbfbfe; --newtab-status-success: #7C6; + --newtab-button-static-background: #2B2A33; + --newtab-button-static-hover-background: #52525E; + --newtab-button-static-active-background: #5B5B66; } @media (prefers-contrast) { @@ -96,7 +109,7 @@ input { background-size: 16px; -moz-context-properties: fill; display: inline-block; - color: var(--newtab-text-primary-color); + color: var(--icon-color); fill: currentColor; height: 16px; vertical-align: middle; @@ -146,6 +159,9 @@ input { .icon.icon-info { background-image: url("chrome://global/skin/icons/info.svg"); } +.icon.icon-info-critical { + background-image: url("chrome://activity-stream/content/data/content/assets/glyph-info-critical-16.svg"); +} .icon.icon-help { background-image: url("chrome://global/skin/icons/help.svg"); } @@ -226,6 +242,9 @@ input { .icon.icon-webextension { background-image: url("chrome://activity-stream/content/data/content/assets/glyph-webextension-16.svg"); } +.icon.icon-weather { + background-image: url("chrome://browser/skin/weather/sunny.svg"); +} .icon.icon-highlights { background-image: url("chrome://global/skin/icons/highlights.svg"); } @@ -663,12 +682,17 @@ main section { .top-site-outer:is(:hover) .context-menu-button { opacity: 1; } +.top-site-outer.active .context-menu-button { + opacity: 1; + background-color: var(--newtab-button-active-background); +} .top-site-outer .context-menu-button { background-image: url("chrome://global/skin/icons/more.svg"); + background-color: var(--newtab-button-background); border: 0; border-radius: 4px; cursor: pointer; - fill: var(--newtab-text-primary-color); + fill: var(--newtab-button-text); -moz-context-properties: fill; height: 20px; width: 20px; @@ -678,11 +702,16 @@ main section { top: -20px; transition: opacity 200ms; } -.top-site-outer .context-menu-button:is(:active, :focus) { - outline: 0; +.top-site-outer .context-menu-button:hover { + background-color: var(--newtab-button-hover-background); +} +.top-site-outer .context-menu-button:hover:active { + background-color: var(--newtab-button-active-background); +} +.top-site-outer .context-menu-button:focus-visible { + background-color: var(--newtab-button-focus-background); + border-color: var(--newtab-button-focus-border); opacity: 1; - background-color: var(--newtab-element-hover-color); - fill: var(--newtab-primary-action-background); } .top-site-outer .tile { border-radius: 8px; @@ -1679,7 +1708,8 @@ main section { inset-block: 0; inset-inline-end: 0; z-index: 1001; - padding: 16px; + padding-block: 0 var(--space-large); + padding-inline: var(--space-large); overflow: auto; transform: translateX(435px); visibility: hidden; @@ -1706,9 +1736,16 @@ main section { .customize-menu.customize-animate-exit-active { box-shadow: 0 2px 14px 0 rgba(0, 0, 0, 0.2); } +.customize-menu .close-button-wrapper { + position: sticky; + top: 0; + padding-block-start: var(--space-large); + background-color: var(--newtab-background-color-secondary); + z-index: 1; +} .customize-menu .close-button { margin-inline-start: auto; - margin-bottom: 28px; + margin-inline-end: var(--space-large); white-space: nowrap; display: block; background-color: var(--newtab-element-secondary-color); @@ -1735,7 +1772,7 @@ main section { grid-template-columns: 1fr; grid-template-rows: repeat(4, auto); grid-row-gap: 32px; - padding: 0 16px; + padding: var(--space-large); } .home-section .wallpapers-section h2 { font-size: inherit; @@ -1982,6 +2019,333 @@ main section { text-decoration: none; } +:root { + --newtab-weather-content-font-size: 11px; + --newtab-weather-sponsor-font-size: 8px; +} + +.weather { + font-size: var(--font-size-root); + position: absolute; + left: var(--space-xlarge); + top: var(--space-xlarge); + z-index: 1; +} + +.weatherNotAvailable { + font-size: var(--newtab-weather-content-font-size); + color: var(--text-color-error); + display: flex; + align-items: center; +} +.weatherNotAvailable .icon { + fill: var(--icon-color-critical); + -moz-context-properties: fill; +} + +.weatherCard { + margin-block-end: var(--space-xsmall); + display: flex; + flex-wrap: nowrap; + align-items: stretch; + border-radius: var(--border-radius-medium); + overflow: hidden; +} +.weatherCard:hover ~ .weatherSponsorText, .weatherCard:focus-within ~ .weatherSponsorText { + visibility: visible; +} +.weatherCard:focus-within { + overflow: visible; +} +.weatherCard:hover { + box-shadow: var(--box-shadow-10); + background: var(--background-color-box); +} +.weatherCard a { + color: var(--text-color); +} + +.weatherSponsorText { + visibility: hidden; + font-size: var(--newtab-weather-sponsor-font-size); + color: var(--text-color-deemphasized); +} + +.weatherInfoLink, .weatherButtonContextMenuWrapper { + appearance: none; + background-color: var(--background-color-ghost); + border: 0; + padding: var(--space-small); + cursor: pointer; +} +.weatherInfoLink:hover, .weatherButtonContextMenuWrapper:hover { + background-color: var(--button-background-color-ghost-hover); +} +.weatherInfoLink:hover::after, .weatherButtonContextMenuWrapper:hover::after { + background-color: transparent; +} +.weatherInfoLink:hover:active, .weatherButtonContextMenuWrapper:hover:active { + background-color: var(--button-background-color-ghost-active); +} +.weatherInfoLink:focus-visible, .weatherButtonContextMenuWrapper:focus-visible { + outline: var(--focus-outline); +} +@media (prefers-color-scheme: dark) { + .hasWallpaperDark .weatherInfoLink, .hasWallpaperDark .weatherButtonContextMenuWrapper { + background-color: rgba(35, 34, 43, 0.7); + } + .hasWallpaperDark .weatherInfoLink:hover, .hasWallpaperDark .weatherButtonContextMenuWrapper:hover { + background-color: var(--newtab-button-static-hover-background); + } + .hasWallpaperDark .weatherInfoLink:hover:active, .hasWallpaperDark .weatherButtonContextMenuWrapper:hover:active { + background-color: var(--newtab-button-static-active-background); + } +} +@media (prefers-contrast) and (prefers-color-scheme: dark) { + .hasWallpaperDark .weatherInfoLink, .hasWallpaperDark .weatherButtonContextMenuWrapper { + background-color: var(--background-color-box); + } +} +@media (prefers-color-scheme: light) { + .hasWallpaperLight .weatherInfoLink, .hasWallpaperLight .weatherButtonContextMenuWrapper { + background-color: rgba(255, 255, 255, 0.7); + } + .hasWallpaperLight .weatherInfoLink:hover, .hasWallpaperLight .weatherButtonContextMenuWrapper:hover { + background-color: var(--newtab-button-static-hover-background); + } + .hasWallpaperLight .weatherInfoLink:hover:active, .hasWallpaperLight .weatherButtonContextMenuWrapper:hover:active { + background-color: var(--newtab-button-static-active-background); + } +} +@media (prefers-contrast) and (prefers-color-scheme: light) { + .hasWallpaperLight .weatherInfoLink, .hasWallpaperLight .weatherButtonContextMenuWrapper { + background-color: var(--background-color-box); + } +} + +.weatherInfoLink { + display: flex; + gap: var(--space-medium); + padding: var(--space-small) var(--space-medium); + border-radius: var(--border-radius-medium) 0 0 var(--border-radius-medium); + text-decoration: none; + color: var(--text-color); + min-width: 130px; + max-width: 190px; + text-overflow: ellipsis; +} +@media (min-width: 610px) { + .weatherInfoLink { + min-width: unset; + } +} +.weatherInfoLink:hover ~ .weatherButtonContextMenuWrapper::after { + background-color: transparent; +} +.weatherInfoLink:focus-visible { + border-radius: var(--border-radius-medium); +} +.weatherInfoLink:focus-visible ~ .weatherButtonContextMenuWrapper::after { + background-color: transparent; +} + +.weatherButtonContextMenuWrapper { + position: relative; + cursor: pointer; + border-radius: 0 var(--border-radius-medium) var(--border-radius-medium) 0; + display: flex; + align-items: stretch; + width: 50px; + padding: 0; +} +.weatherButtonContextMenuWrapper::after { + content: ""; + left: 0; + top: 10px; + height: calc(100% - 20px); + width: 1px; + background-color: var(--newtab-button-static-background); + display: block; + position: absolute; + z-index: 0; +} +@media (prefers-color-scheme: dark) { + .weatherButtonContextMenuWrapper::after { + background-color: var(--color-gray-70); + } +} +.weatherButtonContextMenuWrapper:hover::after { + background-color: transparent; +} +.weatherButtonContextMenuWrapper:focus-visible { + border-radius: var(--border-radius-medium); +} +.weatherButtonContextMenuWrapper:focus-visible::after { + background-color: transparent; +} + +.weatherButtonContextMenu { + background-image: url("chrome://global/skin/icons/more.svg"); + background-repeat: no-repeat; + background-size: var(--size-item-small) auto; + background-position: center; + background-color: transparent; + cursor: pointer; + fill: var(--icon-color); + -moz-context-properties: fill; + width: 100%; + height: 100%; + border: 0; + appearance: none; + min-width: var(--size-item-large); +} + +.weatherText { + height: min-content; +} + +.weatherCityRow, .weatherForecastRow, .weatherDetailedSummaryRow { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-small); +} + +.weatherForecastRow { + text-transform: uppercase; + font-weight: var(--font-weight-bold); +} + +.weatherCityRow { + color: var(--text-color-deemphasized); +} + +.weatherCity { + text-overflow: ellipsis; + font-size: var(--font-size-small); +} + +.weatherCityRow + .weatherDetailedSummaryRow { + margin-block-start: var(--space-xsmall); +} + +.weatherDetailedSummaryRow { + font-size: var(--newtab-weather-content-font-size); + gap: var(--space-large); +} + +.weatherHighLowTemps { + display: flex; + gap: var(--space-xxsmall); + text-transform: uppercase; + word-spacing: var(--space-xxsmall); +} + +.weatherTextSummary { + text-align: center; + max-width: 90px; +} + +.weatherTemperature { + font-size: var(--font-size-large); +} + +.weatherIconCol { + width: var(--size-item-large); + height: var(--size-item-large); + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + align-self: center; +} + +.weatherIcon { + width: var(--size-item-large); + height: auto; + vertical-align: middle; +} +@media (prefers-contrast) { + .weatherIcon { + -moz-context-properties: fill, stroke; + fill: currentColor; + stroke: currentColor; + } +} +.weatherIcon.iconId1 { + content: url("chrome://browser/skin/weather/sunny.svg"); +} +.weatherIcon.iconId2 { + content: url("chrome://browser/skin/weather/mostly-sunny.svg"); +} +.weatherIcon:is(.iconId3, .iconId4, .iconId6) { + content: url("chrome://browser/skin/weather/partly-sunny.svg"); +} +.weatherIcon.iconId5 { + content: url("chrome://browser/skin/weather/hazy-sunshine.svg"); +} +.weatherIcon:is(.iconId7, .iconId8) { + content: url("chrome://browser/skin/weather/cloudy.svg"); +} +.weatherIcon.iconId11 { + content: url("chrome://browser/skin/weather/fog.svg"); +} +.weatherIcon.iconId12 { + content: url("chrome://browser/skin/weather/showers.svg"); +} +.weatherIcon:is(.iconId13, .iconId14) { + content: url("chrome://browser/skin/weather/mostly-cloudy-with-showers.svg"); +} +.weatherIcon.iconId15 { + content: url("chrome://browser/skin/weather/thunderstorms.svg"); +} +.weatherIcon:is(.iconId16, .iconId17) { + content: url("chrome://browser/skin/weather/mostly-cloudy-with-thunderstorms.svg"); +} +.weatherIcon.iconId18 { + content: url("chrome://browser/skin/weather/rain.svg"); +} +.weatherIcon:is(.iconId19, .iconId20, .iconId25) { + content: url("chrome://browser/skin/weather/flurries.svg"); +} +.weatherIcon.iconId21 { + content: url("chrome://browser/skin/weather/partly-sunny-with-flurries.svg"); +} +.weatherIcon:is(.iconId22, .iconId23) { + content: url("chrome://browser/skin/weather/snow.svg"); +} +.weatherIcon:is(.iconId24, .iconId31) { + content: url("chrome://browser/skin/weather/ice.svg"); +} +.weatherIcon:is(.iconId26, .iconId29) { + content: url("chrome://browser/skin/weather/freezing-rain.svg"); +} +.weatherIcon.iconId30 { + content: url("chrome://browser/skin/weather/hot.svg"); +} +.weatherIcon.iconId32 { + content: url("chrome://browser/skin/weather/windy.svg"); +} +.weatherIcon.iconId33 { + content: url("chrome://browser/skin/weather/night-clear.svg"); +} +.weatherIcon:is(.iconId34, .iconId35, .iconId36, .iconId38) { + content: url("chrome://browser/skin/weather/night-mostly-clear.svg"); +} +.weatherIcon.iconId37 { + content: url("chrome://browser/skin/weather/night-hazy-moonlight.svg"); +} +.weatherIcon:is(.iconId39, .iconId40) { + content: url("chrome://browser/skin/weather/night-partly-cloudy-with-showers.svg"); + height: var(--size-item-large); +} +.weatherIcon:is(.iconId41, .iconId42) { + content: url("chrome://browser/skin/weather/night-partly-cloudy-with-thunderstorms.svg"); +} +.weatherIcon:is(.iconId43, .iconId44) { + content: url("chrome://browser/skin/weather/night-mostly-cloudy-with-flurries.svg"); +} + /* stylelint-disable max-nesting-depth */ .card-outer { background: var(--newtab-background-color-secondary); @@ -1994,14 +2358,17 @@ main section { } .card-outer .context-menu-button { background-clip: padding-box; - background-color: var(--newtab-background-color-secondary); + background-color: var(--newtab-button-background); background-image: url("chrome://global/skin/icons/more.svg"); background-position: 55%; - border: 1px solid var(--newtab-border-color); + border: 0; + outline: 1px solid var(--newtab-border-color); + outline-width: 0; border-radius: 100%; box-shadow: 0 2px rgba(12, 12, 13, 0.1); cursor: pointer; - fill: var(--newtab-text-primary-color); + color: var(--button-text-color); + fill: var(--newtab-button-text); height: 27px; inset-inline-end: -13.5px; opacity: 0; @@ -2012,10 +2379,21 @@ main section { transition-property: transform, opacity; width: 27px; } -.card-outer .context-menu-button:is(:active, :focus) { +.card-outer .context-menu-button:is(:active, :focus-visible, :hover) { opacity: 1; transform: scale(1); } +.card-outer .context-menu-button:is(:hover) { + background-color: var(--newtab-button-hover-background); +} +.card-outer .context-menu-button:is(:focus-visible) { + outline-color: var(--newtab-button-focus-border); + background-color: var(--newtab-button-focus-background); + outline-width: 4px; +} +.card-outer .context-menu-button:is(:active) { + background-color: var(--newtab-button-active-background); +} .card-outer:is(:focus):not(.placeholder) { border: 0; outline: 0; @@ -2622,6 +3000,15 @@ main section { width: auto; flex-grow: 1; } +.discoverystream-admin .weather-section { + margin-block-end: 24px; +} +.discoverystream-admin .weather-section form { + display: flex; +} +.discoverystream-admin .weather-section form label { + margin-inline-end: 12px; +} .pocket-logged-in-cta { font-size: 13px; @@ -3309,6 +3696,18 @@ main section { .ds-highlights .section .section-list .card-outer a { text-decoration: none; } +.ds-highlights .section .section-list .card-outer .context-menu-button { + background-color: var(--newtab-button-static-background); +} +.ds-highlights .section .section-list .card-outer .context-menu-button:hover { + background-color: var(--newtab-button-static-hover-background); +} +.ds-highlights .section .section-list .card-outer .context-menu-button:hover:active { + background-color: var(--newtab-button-static-active-background); +} +.ds-highlights .section .section-list .card-outer .context-menu-button:focus-visible { + background-color: var(--newtab-button-static-focus-background); +} .ds-highlights .hide-for-narrow { display: block; } @@ -3570,14 +3969,17 @@ main section { .ds-card .context-menu-button, .ds-signup .context-menu-button { background-clip: padding-box; - background-color: var(--newtab-background-color-secondary); + background-color: var(--newtab-button-background); background-image: url("chrome://global/skin/icons/more.svg"); background-position: 55%; - border: 1px solid var(--newtab-border-color); + border: 0; + outline: 1px solid var(--newtab-border-color); + outline-width: 0; border-radius: 100%; box-shadow: 0 2px rgba(12, 12, 13, 0.1); cursor: pointer; - fill: var(--newtab-text-primary-color); + color: var(--button-text-color); + fill: var(--newtab-button-text); height: 27px; inset-inline-end: -13.5px; opacity: 0; @@ -3588,11 +3990,25 @@ main section { transition-property: transform, opacity; width: 27px; } -.ds-card .context-menu-button:is(:active, :focus), -.ds-signup .context-menu-button:is(:active, :focus) { +.ds-card .context-menu-button:is(:active, :focus-visible, :hover), +.ds-signup .context-menu-button:is(:active, :focus-visible, :hover) { opacity: 1; transform: scale(1); } +.ds-card .context-menu-button:is(:hover), +.ds-signup .context-menu-button:is(:hover) { + background-color: var(--newtab-button-hover-background); +} +.ds-card .context-menu-button:is(:focus-visible), +.ds-signup .context-menu-button:is(:focus-visible) { + outline-color: var(--newtab-button-focus-border); + background-color: var(--newtab-button-focus-background); + outline-width: 4px; +} +.ds-card .context-menu-button:is(:active), +.ds-signup .context-menu-button:is(:active) { + background-color: var(--newtab-button-active-background); +} .ds-card .context-menu, .ds-signup .context-menu { opacity: 0; @@ -3667,11 +4083,6 @@ main section { background-size: 15px; fill: #FFF; } -.ds-card .card-stp-button-hover-background .context-menu-button { - position: static; - transition: none; - border-radius: 3px; -} .ds-card .card-stp-button-hover-background .context-menu-position-container { position: relative; } @@ -3694,6 +4105,9 @@ main section { white-space: nowrap; color: #FFF; } +.ds-card .card-stp-button-hover-background .card-stp-button:focus-visible { + outline: 2px solid var(--newtab-button-focus-border); +} .ds-card .card-stp-button-hover-background button, .ds-card .card-stp-button-hover-background .context-menu { pointer-events: auto; @@ -3701,6 +4115,22 @@ main section { .ds-card .card-stp-button-hover-background button { cursor: pointer; } +.ds-card .context-menu-button { + position: static; + transition: none; + border-radius: 3px; + background-color: var(--newtab-button-static-background); +} +.ds-card .context-menu-button:hover { + background-color: var(--newtab-button-static-hover-background); +} +.ds-card .context-menu-button:hover:active { + background-color: var(--newtab-button-static-active-background); +} +.ds-card .context-menu-button:focus-visible { + outline: 2px solid var(--newtab-button-focus-border); + background-color: var(--newtab-button-static-focus-background); +} .ds-card.last-item .card-stp-button-hover-background .context-menu { margin-inline-start: auto; margin-inline-end: 18.5px; diff --git a/browser/components/newtab/css/activity-stream-windows.css b/browser/components/newtab/css/activity-stream-windows.css index f6118e3c18..96b27e6b5f 100644 --- a/browser/components/newtab/css/activity-stream-windows.css +++ b/browser/components/newtab/css/activity-stream-windows.css @@ -42,6 +42,16 @@ input { --newtab-text-secondary-color: color-mix(in srgb, var(--newtab-text-primary-color) 70%, transparent); --newtab-element-hover-color: color-mix(in srgb, var(--newtab-background-color) 90%, #000); --newtab-element-active-color: color-mix(in srgb, var(--newtab-background-color) 80%, #000); + --newtab-button-background: var(--button-background-color); + --newtab-button-focus-background: var(--newtab-button-background); + --newtab-button-focus-border: var(--focus-outline-color); + --newtab-button-hover-background: var(--button-background-color-hover); + --newtab-button-active-background: var(--button-background-color-active); + --newtab-button-text: var(--button-text-color); + --newtab-button-static-background: #F0F0F4; + --newtab-button-static-focus-background: var(--newtab-button-static-background); + --newtab-button-static-hover-background: #E0E0E6; + --newtab-button-static-active-background: #CFCFD8; --newtab-element-secondary-color: color-mix(in srgb, currentColor 5%, transparent); --newtab-element-secondary-hover-color: color-mix(in srgb, currentColor 12%, transparent); --newtab-element-secondary-active-color: color-mix(in srgb, currentColor 25%, transparent); @@ -79,6 +89,9 @@ input { --newtab-primary-element-text-color: #2b2a33; --newtab-wordmark-color: #fbfbfe; --newtab-status-success: #7C6; + --newtab-button-static-background: #2B2A33; + --newtab-button-static-hover-background: #52525E; + --newtab-button-static-active-background: #5B5B66; } @media (prefers-contrast) { @@ -92,7 +105,7 @@ input { background-size: 16px; -moz-context-properties: fill; display: inline-block; - color: var(--newtab-text-primary-color); + color: var(--icon-color); fill: currentColor; height: 16px; vertical-align: middle; @@ -142,6 +155,9 @@ input { .icon.icon-info { background-image: url("chrome://global/skin/icons/info.svg"); } +.icon.icon-info-critical { + background-image: url("chrome://activity-stream/content/data/content/assets/glyph-info-critical-16.svg"); +} .icon.icon-help { background-image: url("chrome://global/skin/icons/help.svg"); } @@ -222,6 +238,9 @@ input { .icon.icon-webextension { background-image: url("chrome://activity-stream/content/data/content/assets/glyph-webextension-16.svg"); } +.icon.icon-weather { + background-image: url("chrome://browser/skin/weather/sunny.svg"); +} .icon.icon-highlights { background-image: url("chrome://global/skin/icons/highlights.svg"); } @@ -659,12 +678,17 @@ main section { .top-site-outer:is(:hover) .context-menu-button { opacity: 1; } +.top-site-outer.active .context-menu-button { + opacity: 1; + background-color: var(--newtab-button-active-background); +} .top-site-outer .context-menu-button { background-image: url("chrome://global/skin/icons/more.svg"); + background-color: var(--newtab-button-background); border: 0; border-radius: 4px; cursor: pointer; - fill: var(--newtab-text-primary-color); + fill: var(--newtab-button-text); -moz-context-properties: fill; height: 20px; width: 20px; @@ -674,11 +698,16 @@ main section { top: -20px; transition: opacity 200ms; } -.top-site-outer .context-menu-button:is(:active, :focus) { - outline: 0; +.top-site-outer .context-menu-button:hover { + background-color: var(--newtab-button-hover-background); +} +.top-site-outer .context-menu-button:hover:active { + background-color: var(--newtab-button-active-background); +} +.top-site-outer .context-menu-button:focus-visible { + background-color: var(--newtab-button-focus-background); + border-color: var(--newtab-button-focus-border); opacity: 1; - background-color: var(--newtab-element-hover-color); - fill: var(--newtab-primary-action-background); } .top-site-outer .tile { border-radius: 8px; @@ -1675,7 +1704,8 @@ main section { inset-block: 0; inset-inline-end: 0; z-index: 1001; - padding: 16px; + padding-block: 0 var(--space-large); + padding-inline: var(--space-large); overflow: auto; transform: translateX(435px); visibility: hidden; @@ -1702,9 +1732,16 @@ main section { .customize-menu.customize-animate-exit-active { box-shadow: 0 2px 14px 0 rgba(0, 0, 0, 0.2); } +.customize-menu .close-button-wrapper { + position: sticky; + top: 0; + padding-block-start: var(--space-large); + background-color: var(--newtab-background-color-secondary); + z-index: 1; +} .customize-menu .close-button { margin-inline-start: auto; - margin-bottom: 28px; + margin-inline-end: var(--space-large); white-space: nowrap; display: block; background-color: var(--newtab-element-secondary-color); @@ -1731,7 +1768,7 @@ main section { grid-template-columns: 1fr; grid-template-rows: repeat(4, auto); grid-row-gap: 32px; - padding: 0 16px; + padding: var(--space-large); } .home-section .wallpapers-section h2 { font-size: inherit; @@ -1978,6 +2015,333 @@ main section { text-decoration: none; } +:root { + --newtab-weather-content-font-size: 11px; + --newtab-weather-sponsor-font-size: 8px; +} + +.weather { + font-size: var(--font-size-root); + position: absolute; + left: var(--space-xlarge); + top: var(--space-xlarge); + z-index: 1; +} + +.weatherNotAvailable { + font-size: var(--newtab-weather-content-font-size); + color: var(--text-color-error); + display: flex; + align-items: center; +} +.weatherNotAvailable .icon { + fill: var(--icon-color-critical); + -moz-context-properties: fill; +} + +.weatherCard { + margin-block-end: var(--space-xsmall); + display: flex; + flex-wrap: nowrap; + align-items: stretch; + border-radius: var(--border-radius-medium); + overflow: hidden; +} +.weatherCard:hover ~ .weatherSponsorText, .weatherCard:focus-within ~ .weatherSponsorText { + visibility: visible; +} +.weatherCard:focus-within { + overflow: visible; +} +.weatherCard:hover { + box-shadow: var(--box-shadow-10); + background: var(--background-color-box); +} +.weatherCard a { + color: var(--text-color); +} + +.weatherSponsorText { + visibility: hidden; + font-size: var(--newtab-weather-sponsor-font-size); + color: var(--text-color-deemphasized); +} + +.weatherInfoLink, .weatherButtonContextMenuWrapper { + appearance: none; + background-color: var(--background-color-ghost); + border: 0; + padding: var(--space-small); + cursor: pointer; +} +.weatherInfoLink:hover, .weatherButtonContextMenuWrapper:hover { + background-color: var(--button-background-color-ghost-hover); +} +.weatherInfoLink:hover::after, .weatherButtonContextMenuWrapper:hover::after { + background-color: transparent; +} +.weatherInfoLink:hover:active, .weatherButtonContextMenuWrapper:hover:active { + background-color: var(--button-background-color-ghost-active); +} +.weatherInfoLink:focus-visible, .weatherButtonContextMenuWrapper:focus-visible { + outline: var(--focus-outline); +} +@media (prefers-color-scheme: dark) { + .hasWallpaperDark .weatherInfoLink, .hasWallpaperDark .weatherButtonContextMenuWrapper { + background-color: rgba(35, 34, 43, 0.7); + } + .hasWallpaperDark .weatherInfoLink:hover, .hasWallpaperDark .weatherButtonContextMenuWrapper:hover { + background-color: var(--newtab-button-static-hover-background); + } + .hasWallpaperDark .weatherInfoLink:hover:active, .hasWallpaperDark .weatherButtonContextMenuWrapper:hover:active { + background-color: var(--newtab-button-static-active-background); + } +} +@media (prefers-contrast) and (prefers-color-scheme: dark) { + .hasWallpaperDark .weatherInfoLink, .hasWallpaperDark .weatherButtonContextMenuWrapper { + background-color: var(--background-color-box); + } +} +@media (prefers-color-scheme: light) { + .hasWallpaperLight .weatherInfoLink, .hasWallpaperLight .weatherButtonContextMenuWrapper { + background-color: rgba(255, 255, 255, 0.7); + } + .hasWallpaperLight .weatherInfoLink:hover, .hasWallpaperLight .weatherButtonContextMenuWrapper:hover { + background-color: var(--newtab-button-static-hover-background); + } + .hasWallpaperLight .weatherInfoLink:hover:active, .hasWallpaperLight .weatherButtonContextMenuWrapper:hover:active { + background-color: var(--newtab-button-static-active-background); + } +} +@media (prefers-contrast) and (prefers-color-scheme: light) { + .hasWallpaperLight .weatherInfoLink, .hasWallpaperLight .weatherButtonContextMenuWrapper { + background-color: var(--background-color-box); + } +} + +.weatherInfoLink { + display: flex; + gap: var(--space-medium); + padding: var(--space-small) var(--space-medium); + border-radius: var(--border-radius-medium) 0 0 var(--border-radius-medium); + text-decoration: none; + color: var(--text-color); + min-width: 130px; + max-width: 190px; + text-overflow: ellipsis; +} +@media (min-width: 610px) { + .weatherInfoLink { + min-width: unset; + } +} +.weatherInfoLink:hover ~ .weatherButtonContextMenuWrapper::after { + background-color: transparent; +} +.weatherInfoLink:focus-visible { + border-radius: var(--border-radius-medium); +} +.weatherInfoLink:focus-visible ~ .weatherButtonContextMenuWrapper::after { + background-color: transparent; +} + +.weatherButtonContextMenuWrapper { + position: relative; + cursor: pointer; + border-radius: 0 var(--border-radius-medium) var(--border-radius-medium) 0; + display: flex; + align-items: stretch; + width: 50px; + padding: 0; +} +.weatherButtonContextMenuWrapper::after { + content: ""; + left: 0; + top: 10px; + height: calc(100% - 20px); + width: 1px; + background-color: var(--newtab-button-static-background); + display: block; + position: absolute; + z-index: 0; +} +@media (prefers-color-scheme: dark) { + .weatherButtonContextMenuWrapper::after { + background-color: var(--color-gray-70); + } +} +.weatherButtonContextMenuWrapper:hover::after { + background-color: transparent; +} +.weatherButtonContextMenuWrapper:focus-visible { + border-radius: var(--border-radius-medium); +} +.weatherButtonContextMenuWrapper:focus-visible::after { + background-color: transparent; +} + +.weatherButtonContextMenu { + background-image: url("chrome://global/skin/icons/more.svg"); + background-repeat: no-repeat; + background-size: var(--size-item-small) auto; + background-position: center; + background-color: transparent; + cursor: pointer; + fill: var(--icon-color); + -moz-context-properties: fill; + width: 100%; + height: 100%; + border: 0; + appearance: none; + min-width: var(--size-item-large); +} + +.weatherText { + height: min-content; +} + +.weatherCityRow, .weatherForecastRow, .weatherDetailedSummaryRow { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-small); +} + +.weatherForecastRow { + text-transform: uppercase; + font-weight: var(--font-weight-bold); +} + +.weatherCityRow { + color: var(--text-color-deemphasized); +} + +.weatherCity { + text-overflow: ellipsis; + font-size: var(--font-size-small); +} + +.weatherCityRow + .weatherDetailedSummaryRow { + margin-block-start: var(--space-xsmall); +} + +.weatherDetailedSummaryRow { + font-size: var(--newtab-weather-content-font-size); + gap: var(--space-large); +} + +.weatherHighLowTemps { + display: flex; + gap: var(--space-xxsmall); + text-transform: uppercase; + word-spacing: var(--space-xxsmall); +} + +.weatherTextSummary { + text-align: center; + max-width: 90px; +} + +.weatherTemperature { + font-size: var(--font-size-large); +} + +.weatherIconCol { + width: var(--size-item-large); + height: var(--size-item-large); + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + align-self: center; +} + +.weatherIcon { + width: var(--size-item-large); + height: auto; + vertical-align: middle; +} +@media (prefers-contrast) { + .weatherIcon { + -moz-context-properties: fill, stroke; + fill: currentColor; + stroke: currentColor; + } +} +.weatherIcon.iconId1 { + content: url("chrome://browser/skin/weather/sunny.svg"); +} +.weatherIcon.iconId2 { + content: url("chrome://browser/skin/weather/mostly-sunny.svg"); +} +.weatherIcon:is(.iconId3, .iconId4, .iconId6) { + content: url("chrome://browser/skin/weather/partly-sunny.svg"); +} +.weatherIcon.iconId5 { + content: url("chrome://browser/skin/weather/hazy-sunshine.svg"); +} +.weatherIcon:is(.iconId7, .iconId8) { + content: url("chrome://browser/skin/weather/cloudy.svg"); +} +.weatherIcon.iconId11 { + content: url("chrome://browser/skin/weather/fog.svg"); +} +.weatherIcon.iconId12 { + content: url("chrome://browser/skin/weather/showers.svg"); +} +.weatherIcon:is(.iconId13, .iconId14) { + content: url("chrome://browser/skin/weather/mostly-cloudy-with-showers.svg"); +} +.weatherIcon.iconId15 { + content: url("chrome://browser/skin/weather/thunderstorms.svg"); +} +.weatherIcon:is(.iconId16, .iconId17) { + content: url("chrome://browser/skin/weather/mostly-cloudy-with-thunderstorms.svg"); +} +.weatherIcon.iconId18 { + content: url("chrome://browser/skin/weather/rain.svg"); +} +.weatherIcon:is(.iconId19, .iconId20, .iconId25) { + content: url("chrome://browser/skin/weather/flurries.svg"); +} +.weatherIcon.iconId21 { + content: url("chrome://browser/skin/weather/partly-sunny-with-flurries.svg"); +} +.weatherIcon:is(.iconId22, .iconId23) { + content: url("chrome://browser/skin/weather/snow.svg"); +} +.weatherIcon:is(.iconId24, .iconId31) { + content: url("chrome://browser/skin/weather/ice.svg"); +} +.weatherIcon:is(.iconId26, .iconId29) { + content: url("chrome://browser/skin/weather/freezing-rain.svg"); +} +.weatherIcon.iconId30 { + content: url("chrome://browser/skin/weather/hot.svg"); +} +.weatherIcon.iconId32 { + content: url("chrome://browser/skin/weather/windy.svg"); +} +.weatherIcon.iconId33 { + content: url("chrome://browser/skin/weather/night-clear.svg"); +} +.weatherIcon:is(.iconId34, .iconId35, .iconId36, .iconId38) { + content: url("chrome://browser/skin/weather/night-mostly-clear.svg"); +} +.weatherIcon.iconId37 { + content: url("chrome://browser/skin/weather/night-hazy-moonlight.svg"); +} +.weatherIcon:is(.iconId39, .iconId40) { + content: url("chrome://browser/skin/weather/night-partly-cloudy-with-showers.svg"); + height: var(--size-item-large); +} +.weatherIcon:is(.iconId41, .iconId42) { + content: url("chrome://browser/skin/weather/night-partly-cloudy-with-thunderstorms.svg"); +} +.weatherIcon:is(.iconId43, .iconId44) { + content: url("chrome://browser/skin/weather/night-mostly-cloudy-with-flurries.svg"); +} + /* stylelint-disable max-nesting-depth */ .card-outer { background: var(--newtab-background-color-secondary); @@ -1990,14 +2354,17 @@ main section { } .card-outer .context-menu-button { background-clip: padding-box; - background-color: var(--newtab-background-color-secondary); + background-color: var(--newtab-button-background); background-image: url("chrome://global/skin/icons/more.svg"); background-position: 55%; - border: 1px solid var(--newtab-border-color); + border: 0; + outline: 1px solid var(--newtab-border-color); + outline-width: 0; border-radius: 100%; box-shadow: 0 2px rgba(12, 12, 13, 0.1); cursor: pointer; - fill: var(--newtab-text-primary-color); + color: var(--button-text-color); + fill: var(--newtab-button-text); height: 27px; inset-inline-end: -13.5px; opacity: 0; @@ -2008,10 +2375,21 @@ main section { transition-property: transform, opacity; width: 27px; } -.card-outer .context-menu-button:is(:active, :focus) { +.card-outer .context-menu-button:is(:active, :focus-visible, :hover) { opacity: 1; transform: scale(1); } +.card-outer .context-menu-button:is(:hover) { + background-color: var(--newtab-button-hover-background); +} +.card-outer .context-menu-button:is(:focus-visible) { + outline-color: var(--newtab-button-focus-border); + background-color: var(--newtab-button-focus-background); + outline-width: 4px; +} +.card-outer .context-menu-button:is(:active) { + background-color: var(--newtab-button-active-background); +} .card-outer:is(:focus):not(.placeholder) { border: 0; outline: 0; @@ -2618,6 +2996,15 @@ main section { width: auto; flex-grow: 1; } +.discoverystream-admin .weather-section { + margin-block-end: 24px; +} +.discoverystream-admin .weather-section form { + display: flex; +} +.discoverystream-admin .weather-section form label { + margin-inline-end: 12px; +} .pocket-logged-in-cta { font-size: 13px; @@ -3305,6 +3692,18 @@ main section { .ds-highlights .section .section-list .card-outer a { text-decoration: none; } +.ds-highlights .section .section-list .card-outer .context-menu-button { + background-color: var(--newtab-button-static-background); +} +.ds-highlights .section .section-list .card-outer .context-menu-button:hover { + background-color: var(--newtab-button-static-hover-background); +} +.ds-highlights .section .section-list .card-outer .context-menu-button:hover:active { + background-color: var(--newtab-button-static-active-background); +} +.ds-highlights .section .section-list .card-outer .context-menu-button:focus-visible { + background-color: var(--newtab-button-static-focus-background); +} .ds-highlights .hide-for-narrow { display: block; } @@ -3566,14 +3965,17 @@ main section { .ds-card .context-menu-button, .ds-signup .context-menu-button { background-clip: padding-box; - background-color: var(--newtab-background-color-secondary); + background-color: var(--newtab-button-background); background-image: url("chrome://global/skin/icons/more.svg"); background-position: 55%; - border: 1px solid var(--newtab-border-color); + border: 0; + outline: 1px solid var(--newtab-border-color); + outline-width: 0; border-radius: 100%; box-shadow: 0 2px rgba(12, 12, 13, 0.1); cursor: pointer; - fill: var(--newtab-text-primary-color); + color: var(--button-text-color); + fill: var(--newtab-button-text); height: 27px; inset-inline-end: -13.5px; opacity: 0; @@ -3584,11 +3986,25 @@ main section { transition-property: transform, opacity; width: 27px; } -.ds-card .context-menu-button:is(:active, :focus), -.ds-signup .context-menu-button:is(:active, :focus) { +.ds-card .context-menu-button:is(:active, :focus-visible, :hover), +.ds-signup .context-menu-button:is(:active, :focus-visible, :hover) { opacity: 1; transform: scale(1); } +.ds-card .context-menu-button:is(:hover), +.ds-signup .context-menu-button:is(:hover) { + background-color: var(--newtab-button-hover-background); +} +.ds-card .context-menu-button:is(:focus-visible), +.ds-signup .context-menu-button:is(:focus-visible) { + outline-color: var(--newtab-button-focus-border); + background-color: var(--newtab-button-focus-background); + outline-width: 4px; +} +.ds-card .context-menu-button:is(:active), +.ds-signup .context-menu-button:is(:active) { + background-color: var(--newtab-button-active-background); +} .ds-card .context-menu, .ds-signup .context-menu { opacity: 0; @@ -3663,11 +4079,6 @@ main section { background-size: 15px; fill: #FFF; } -.ds-card .card-stp-button-hover-background .context-menu-button { - position: static; - transition: none; - border-radius: 3px; -} .ds-card .card-stp-button-hover-background .context-menu-position-container { position: relative; } @@ -3690,6 +4101,9 @@ main section { white-space: nowrap; color: #FFF; } +.ds-card .card-stp-button-hover-background .card-stp-button:focus-visible { + outline: 2px solid var(--newtab-button-focus-border); +} .ds-card .card-stp-button-hover-background button, .ds-card .card-stp-button-hover-background .context-menu { pointer-events: auto; @@ -3697,6 +4111,22 @@ main section { .ds-card .card-stp-button-hover-background button { cursor: pointer; } +.ds-card .context-menu-button { + position: static; + transition: none; + border-radius: 3px; + background-color: var(--newtab-button-static-background); +} +.ds-card .context-menu-button:hover { + background-color: var(--newtab-button-static-hover-background); +} +.ds-card .context-menu-button:hover:active { + background-color: var(--newtab-button-static-active-background); +} +.ds-card .context-menu-button:focus-visible { + outline: 2px solid var(--newtab-button-focus-border); + background-color: var(--newtab-button-static-focus-background); +} .ds-card.last-item .card-stp-button-hover-background .context-menu { margin-inline-start: auto; margin-inline-end: 18.5px; diff --git a/browser/components/newtab/data/content/activity-stream.bundle.js b/browser/components/newtab/data/content/activity-stream.bundle.js index 395e8c5bb3..45705fe58c 100644 --- a/browser/components/newtab/data/content/activity-stream.bundle.js +++ b/browser/components/newtab/data/content/activity-stream.bundle.js @@ -234,6 +234,11 @@ for (const type of [ "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", "WALLPAPERS_SET", + "WALLPAPER_CLICK", + "WEATHER_IMPRESSION", + "WEATHER_LOAD_ERROR", + "WEATHER_OPEN_PROVIDER_URL", + "WEATHER_UPDATE", "WEBEXT_CLICK", "WEBEXT_DISMISS", ]) { @@ -675,8 +680,11 @@ class DiscoveryStreamAdminUI extends (external_React_default()).PureComponent { this.systemTick = this.systemTick.bind(this); this.syncRemoteSettings = this.syncRemoteSettings.bind(this); this.onStoryToggle = this.onStoryToggle.bind(this); + this.handleWeatherSubmit = this.handleWeatherSubmit.bind(this); + this.handleWeatherUpdate = this.handleWeatherUpdate.bind(this); this.state = { - toggledStories: {} + toggledStories: {}, + weatherQuery: "" }; } setConfigValue(name, value) { @@ -719,6 +727,18 @@ class DiscoveryStreamAdminUI extends (external_React_default()).PureComponent { syncRemoteSettings() { this.dispatchSimpleAction(actionTypes.DISCOVERY_STREAM_DEV_SYNC_RS); } + handleWeatherUpdate(e) { + this.setState({ + weatherQuery: e.target.value || "" + }); + } + handleWeatherSubmit(e) { + e.preventDefault(); + const { + weatherQuery + } = this.state; + this.props.dispatch(actionCreators.SetPref("weather.query", weatherQuery)); + } renderComponent(width, component) { return /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", { className: "min" @@ -726,6 +746,38 @@ class DiscoveryStreamAdminUI extends (external_React_default()).PureComponent { className: "min" }, "Width"), /*#__PURE__*/external_React_default().createElement("td", null, width)), component.feed && this.renderFeed(component.feed))); } + renderWeatherData() { + const { + suggestions + } = this.props.state.Weather; + let weatherTable; + if (suggestions) { + weatherTable = /*#__PURE__*/external_React_default().createElement("div", { + className: "weather-section" + }, /*#__PURE__*/external_React_default().createElement("form", { + onSubmit: this.handleWeatherSubmit + }, /*#__PURE__*/external_React_default().createElement("label", { + htmlFor: "weather-query" + }, "Weather query"), /*#__PURE__*/external_React_default().createElement("input", { + type: "text", + min: "3", + max: "10", + id: "weather-query", + onChange: this.handleWeatherUpdate, + value: this.weatherQuery + }), /*#__PURE__*/external_React_default().createElement("button", { + type: "submit" + }, "Submit")), /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, suggestions.map(suggestion => /*#__PURE__*/external_React_default().createElement("tr", { + className: "message-item", + key: suggestion.city_name + }, /*#__PURE__*/external_React_default().createElement("td", { + className: "message-id" + }, /*#__PURE__*/external_React_default().createElement("span", null, suggestion.city_name, " ", /*#__PURE__*/external_React_default().createElement("br", null))), /*#__PURE__*/external_React_default().createElement("td", { + className: "message-summary" + }, /*#__PURE__*/external_React_default().createElement("pre", null, JSON.stringify(suggestion, null, 2)))))))); + } + return weatherTable; + } renderFeedData(url) { const { feeds @@ -836,7 +888,7 @@ class DiscoveryStreamAdminUI extends (external_React_default()).PureComponent { state: { Personalization: this.props.state.Personalization } - }), /*#__PURE__*/external_React_default().createElement("h3", null, "Spocs"), this.renderSpocs(), /*#__PURE__*/external_React_default().createElement("h3", null, "Feeds Data"), this.renderFeedsData()); + }), /*#__PURE__*/external_React_default().createElement("h3", null, "Spocs"), this.renderSpocs(), /*#__PURE__*/external_React_default().createElement("h3", null, "Feeds Data"), this.renderFeedsData(), /*#__PURE__*/external_React_default().createElement("h3", null, "Weather Data"), this.renderWeatherData()); } } class DiscoveryStreamAdminInner extends (external_React_default()).PureComponent { @@ -859,7 +911,8 @@ class DiscoveryStreamAdminInner extends (external_React_default()).PureComponent }, "Click here"))), /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement(DiscoveryStreamAdminUI, { state: { DiscoveryStream: this.props.DiscoveryStream, - Personalization: this.props.Personalization + Personalization: this.props.Personalization, + Weather: this.props.Weather }, otherPrefs: this.props.Prefs.values, dispatch: this.props.dispatch @@ -929,7 +982,8 @@ const DiscoveryStreamAdmin = (0,external_ReactRedux_namespaceObject.connect)(sta Sections: state.Sections, DiscoveryStream: state.DiscoveryStream, Personalization: state.Personalization, - Prefs: state.Prefs + Prefs: state.Prefs, + Weather: state.Weather }))(_DiscoveryStreamAdmin); ;// CONCATENATED MODULE: ./content-src/components/ConfirmDialog/ConfirmDialog.jsx /* This Source Code Form is subject to the terms of the Mozilla Public @@ -1704,6 +1758,70 @@ const LinkMenuOptions = { : LinkMenuOptions.EmptyItem(), OpenInPrivateWindow: (site, index, eventSource, isEnabled) => isEnabled ? _OpenInPrivateWindow(site) : LinkMenuOptions.EmptyItem(), + ChangeWeatherLocation: () => ({ + id: "newtab-weather-menu-change-location", + action: actionCreators.OnlyToMain({ + type: actionTypes.CHANGE_WEATHER_LOCATION, + data: { url: "https://mozilla.org" }, + }), + }), + ChangeWeatherDisplaySimple: () => ({ + id: "newtab-weather-menu-change-weather-display-simple", + action: actionCreators.OnlyToMain({ + type: actionTypes.SET_PREF, + data: { + name: "weather.display", + value: "simple", + }, + }), + }), + ChangeWeatherDisplayDetailed: () => ({ + id: "newtab-weather-menu-change-weather-display-detailed", + action: actionCreators.OnlyToMain({ + type: actionTypes.SET_PREF, + data: { + name: "weather.display", + value: "detailed", + }, + }), + }), + ChangeTempUnitFahrenheit: () => ({ + id: "newtab-weather-menu-change-temperature-units-fahrenheit", + action: actionCreators.OnlyToMain({ + type: actionTypes.SET_PREF, + data: { + name: "weather.temperatureUnits", + value: "f", + }, + }), + }), + ChangeTempUnitCelsius: () => ({ + id: "newtab-weather-menu-change-temperature-units-celsius", + action: actionCreators.OnlyToMain({ + type: actionTypes.SET_PREF, + data: { + name: "weather.temperatureUnits", + value: "c", + }, + }), + }), + HideWeather: () => ({ + id: "newtab-weather-menu-hide-weather", + action: actionCreators.OnlyToMain({ + type: actionTypes.SET_PREF, + data: { + name: "showWeather", + value: false, + }, + }), + }), + OpenLearnMoreURL: site => ({ + id: "newtab-weather-menu-learn-more", + action: actionCreators.OnlyToMain({ + type: actionTypes.OPEN_LINK, + data: { url: site.url }, + }), + }), }; ;// CONCATENATED MODULE: ./content-src/components/LinkMenu/LinkMenu.jsx @@ -2967,11 +3085,11 @@ class _DSCard extends (external_React_default()).PureComponent { ctaButtonVariant: ctaButtonVariant, dispatch: this.props.dispatch, spocMessageVariant: this.props.spocMessageVariant - }), saveToPocketCard && /*#__PURE__*/external_React_default().createElement("div", { + }), /*#__PURE__*/external_React_default().createElement("div", { className: "card-stp-button-hover-background" }, /*#__PURE__*/external_React_default().createElement("div", { className: "card-stp-button-position-wrapper" - }, !this.props.flightId && stpButton(), /*#__PURE__*/external_React_default().createElement(DSLinkMenu, { + }, saveToPocketCard && /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, !this.props.flightId && stpButton()), /*#__PURE__*/external_React_default().createElement(DSLinkMenu, { id: this.props.id, index: this.props.pos, dispatch: this.props.dispatch, @@ -2989,25 +3107,7 @@ class _DSCard extends (external_React_default()).PureComponent { saveToPocketCard: saveToPocketCard, pocket_button_enabled: pocketButtonEnabled, isRecentSave: isRecentSave - }))), !saveToPocketCard && /*#__PURE__*/external_React_default().createElement(DSLinkMenu, { - id: this.props.id, - index: this.props.pos, - dispatch: this.props.dispatch, - url: this.props.url, - title: this.props.title, - source: source, - type: this.props.type, - pocket_id: this.props.pocket_id, - shim: this.props.shim, - bookmarkGuid: this.props.bookmarkGuid, - flightId: !this.props.is_collection ? this.props.flightId : undefined, - showPrivacyInfo: !!this.props.flightId, - hostRef: this.contextMenuButtonHostRef, - onMenuUpdate: this.onMenuUpdate, - onMenuShow: this.onMenuShow, - pocket_button_enabled: pocketButtonEnabled, - isRecentSave: isRecentSave - })); + })))); } } _DSCard.defaultProps = { @@ -5534,6 +5634,12 @@ const INITIAL_STATE = { Wallpapers: { wallpaperList: [], }, + Weather: { + // do we have the data from WeatherFeed yet? + initialized: false, + suggestions: [], + lastUpdated: null, + }, }; function App(prevState = INITIAL_STATE.App, action) { @@ -6283,6 +6389,20 @@ function Wallpapers(prevState = INITIAL_STATE.Wallpapers, action) { } } +function Weather(prevState = INITIAL_STATE.Weather, action) { + switch (action.type) { + case actionTypes.WEATHER_UPDATE: + return { + ...prevState, + suggestions: action.data.suggestions, + lastUpdated: action.data.date, + initialized: true, + }; + default: + return prevState; + } +} + const reducers = { TopSites, App, @@ -6295,6 +6415,7 @@ const reducers = { DiscoveryStream, Search, Wallpapers, + Weather, }; ;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteFormInput.jsx @@ -8854,6 +8975,7 @@ const DiscoveryStreamBase = (0,external_ReactRedux_namespaceObject.connect)(stat + class _WallpapersSection extends (external_React_default()).PureComponent { constructor(props) { super(props); @@ -8872,6 +8994,10 @@ class _WallpapersSection extends (external_React_default()).PureComponent { const prefs = this.props.Prefs.values; const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; this.props.setPref(`newtabWallpapers.wallpaper-${colorMode}`, id); + this.handleUserEvent({ + selected_wallpaper: id, + hadPreviousWallpaper: !!this.props.activeWallpaper + }); // bug 1892095 if (prefs["newtabWallpapers.wallpaper-dark"] === "" && colorMode === "light") { this.props.setPref("newtabWallpapers.wallpaper-dark", id.replace("light", "dark")); @@ -8883,6 +9009,18 @@ class _WallpapersSection extends (external_React_default()).PureComponent { handleReset() { const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; this.props.setPref(`newtabWallpapers.wallpaper-${colorMode}`, ""); + this.handleUserEvent({ + selected_wallpaper: "none", + hadPreviousWallpaper: !!this.props.activeWallpaper + }); + } + + // Record user interaction when changing wallpaper and reseting wallpaper to default + handleUserEvent(data) { + this.props.dispatch(actionCreators.OnlyToMain({ + type: actionTypes.WALLPAPER_CLICK, + data + })); } render() { const { @@ -8954,7 +9092,7 @@ class ContentSection extends (external_React_default()).PureComponent { })); } onPreferenceSelect(e) { - // eventSource: TOP_SITES | TOP_STORIES | HIGHLIGHTS + // eventSource: TOP_SITES | TOP_STORIES | HIGHLIGHTS | WEATHER const { preference, eventSource @@ -9011,6 +9149,7 @@ class ContentSection extends (external_React_default()).PureComponent { pocketRegion, mayHaveSponsoredStories, mayHaveRecentSaves, + mayHaveWeather, openPreferences, spocMessageVariant, wallpapersEnabled, @@ -9021,6 +9160,7 @@ class ContentSection extends (external_React_default()).PureComponent { topSitesEnabled, pocketEnabled, highlightsEnabled, + weatherEnabled, showSponsoredTopSitesEnabled, showSponsoredPocketEnabled, showRecentSavesEnabled, @@ -9156,6 +9296,19 @@ class ContentSection extends (external_React_default()).PureComponent { "data-eventSource": "HIGHLIGHTS", "data-l10n-id": "newtab-custom-recent-toggle", "data-l10n-attrs": "label, description" + }))), mayHaveWeather && /*#__PURE__*/external_React_default().createElement("div", { + id: "weather-section", + className: "section" + }, /*#__PURE__*/external_React_default().createElement("label", { + className: "switch" + }, /*#__PURE__*/external_React_default().createElement("moz-toggle", { + id: "weather-toggle", + pressed: weatherEnabled || null, + onToggle: this.onPreferenceSelect, + "data-preference": "showWeather", + "data-eventSource": "WEATHER", + "data-l10n-id": "newtab-custom-weather-toggle", + "data-l10n-attrs": "label, description" }))), pocketRegion && mayHaveSponsoredStories && spocMessageVariant === "variant-c" && /*#__PURE__*/external_React_default().createElement("div", { className: "sponsored-content-info" }, /*#__PURE__*/external_React_default().createElement("div", { @@ -9221,12 +9374,14 @@ class _CustomizeMenu extends (external_React_default()).PureComponent { className: "customize-menu", role: "dialog", "data-l10n-id": "newtab-personalize-dialog-label" + }, /*#__PURE__*/external_React_default().createElement("div", { + className: "close-button-wrapper" }, /*#__PURE__*/external_React_default().createElement("button", { onClick: () => this.props.onClose(), className: "close-button", "data-l10n-id": "newtab-custom-close-button", ref: c => this.closeButton = c - }), /*#__PURE__*/external_React_default().createElement(ContentSection, { + })), /*#__PURE__*/external_React_default().createElement(ContentSection, { openPreferences: this.props.openPreferences, setPref: this.props.setPref, enabledSections: this.props.enabledSections, @@ -9236,6 +9391,7 @@ class _CustomizeMenu extends (external_React_default()).PureComponent { mayHaveSponsoredTopSites: this.props.mayHaveSponsoredTopSites, mayHaveSponsoredStories: this.props.mayHaveSponsoredStories, mayHaveRecentSaves: this.props.DiscoveryStream.recentSavesEnabled, + mayHaveWeather: this.props.mayHaveWeather, spocMessageVariant: this.props.spocMessageVariant, dispatch: this.props.dispatch })))); @@ -9449,6 +9605,260 @@ class _Search extends (external_React_default()).PureComponent { const Search_Search = (0,external_ReactRedux_namespaceObject.connect)(state => ({ Prefs: state.Prefs }))(_Search); +;// CONCATENATED MODULE: ./content-src/components/Weather/Weather.jsx +/* 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 Weather_VISIBLE = "visible"; +const Weather_VISIBILITY_CHANGE_EVENT = "visibilitychange"; +class _Weather extends (external_React_default()).PureComponent { + constructor(props) { + super(props); + this.state = { + contextMenuKeyboard: false, + showContextMenu: false, + url: "https://example.com", + impressionSeen: false, + errorSeen: false + }; + this.setImpressionRef = element => { + this.impressionElement = element; + }; + this.setErrorRef = element => { + this.errorElement = element; + }; + this.onClick = this.onClick.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.onUpdate = this.onUpdate.bind(this); + this.onProviderClick = this.onProviderClick.bind(this); + } + componentDidMount() { + const { + props + } = this; + if (!props.dispatch) { + return; + } + if (props.document.visibilityState === Weather_VISIBLE) { + // Setup the impression observer once the page is visible. + this.setImpressionObservers(); + } else { + // We should only ever send the latest impression stats ping, so remove any + // older listeners. + if (this._onVisibilityChange) { + props.document.removeEventListener(Weather_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); + } + this._onVisibilityChange = () => { + if (props.document.visibilityState === Weather_VISIBLE) { + // Setup the impression observer once the page is visible. + this.setImpressionObservers(); + props.document.removeEventListener(Weather_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); + } + }; + props.document.addEventListener(Weather_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); + } + } + componentWillUnmount() { + // Remove observers on unmount + if (this.observer && this.impressionElement) { + this.observer.unobserve(this.impressionElement); + } + if (this.observer && this.errorElement) { + this.observer.unobserve(this.errorElement); + } + if (this._onVisibilityChange) { + this.props.document.removeEventListener(Weather_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); + } + } + setImpressionObservers() { + if (this.impressionElement) { + this.observer = new IntersectionObserver(this.onImpression.bind(this)); + this.observer.observe(this.impressionElement); + } + if (this.errorElement) { + this.observer = new IntersectionObserver(this.onError.bind(this)); + this.observer.observe(this.errorElement); + } + } + onImpression(entries) { + if (this.state) { + const entry = entries.find(e => e.isIntersecting); + if (entry) { + if (this.impressionElement) { + this.observer.unobserve(this.impressionElement); + } + this.props.dispatch(actionCreators.OnlyToMain({ + type: actionTypes.WEATHER_IMPRESSION + })); + + // Stop observing since element has been seen + this.setState({ + impressionSeen: true + }); + } + } + } + onError(entries) { + if (this.state) { + const entry = entries.find(e => e.isIntersecting); + if (entry) { + if (this.errorElement) { + this.observer.unobserve(this.errorElement); + } + this.props.dispatch(actionCreators.OnlyToMain({ + type: actionTypes.WEATHER_LOAD_ERROR + })); + + // Stop observing since element has been seen + this.setState({ + errorSeen: true + }); + } + } + } + openContextMenu(isKeyBoard) { + if (this.props.onUpdate) { + this.props.onUpdate(true); + } + this.setState({ + showContextMenu: true, + contextMenuKeyboard: isKeyBoard + }); + } + onClick(event) { + event.preventDefault(); + this.openContextMenu(false, event); + } + onKeyDown(event) { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + this.openContextMenu(true, event); + } + } + onUpdate(showContextMenu) { + if (this.props.onUpdate) { + this.props.onUpdate(showContextMenu); + } + this.setState({ + showContextMenu + }); + } + onProviderClick() { + this.props.dispatch(actionCreators.OnlyToMain({ + type: actionTypes.WEATHER_OPEN_PROVIDER_URL, + data: { + source: "WEATHER" + } + })); + } + render() { + // Check if weather should be rendered + const isWeatherEnabled = this.props.Prefs.values["system.showWeather"]; + if (!isWeatherEnabled || !this.props.Weather.initialized) { + return false; + } + const { + showContextMenu + } = this.state; + const WEATHER_SUGGESTION = this.props.Weather.suggestions?.[0]; + const { + className, + index, + dispatch, + eventSource, + shouldSendImpressionStats + } = this.props; + const { + props + } = this; + const isContextMenuOpen = this.state.activeCard === index; + const outerClassName = ["weather", className, isContextMenuOpen && "active", props.placeholder && "placeholder"].filter(v => v).join(" "); + const showDetailedView = this.props.Prefs.values["weather.display"] === "detailed"; + + // Note: The temperature units/display options will become secondary menu items + const WEATHER_SOURCE_CONTEXT_MENU_OPTIONS = [...(this.props.Prefs.values["weather.locationSearchEnabled"] ? ["ChangeWeatherLocation"] : []), ...(this.props.Prefs.values["weather.temperatureUnits"] === "f" ? ["ChangeTempUnitCelsius"] : ["ChangeTempUnitFahrenheit"]), ...(this.props.Prefs.values["weather.display"] === "simple" ? ["ChangeWeatherDisplayDetailed"] : ["ChangeWeatherDisplaySimple"]), "HideWeather", "OpenLearnMoreURL"]; + + // Only return the widget if we have data. Otherwise, show error state + if (WEATHER_SUGGESTION) { + return /*#__PURE__*/external_React_default().createElement("div", { + ref: this.setImpressionRef, + className: outerClassName + }, /*#__PURE__*/external_React_default().createElement("div", { + className: "weatherCard" + }, /*#__PURE__*/external_React_default().createElement("a", { + "data-l10n-id": "newtab-weather-see-forecast", + "data-l10n-args": "{\"provider\": \"AccuWeather\"}", + href: WEATHER_SUGGESTION.forecast.url, + className: "weatherInfoLink", + onClick: this.onProviderClick + }, /*#__PURE__*/external_React_default().createElement("div", { + className: "weatherIconCol" + }, /*#__PURE__*/external_React_default().createElement("span", { + className: `weatherIcon iconId${WEATHER_SUGGESTION.current_conditions.icon_id}` + })), /*#__PURE__*/external_React_default().createElement("div", { + className: "weatherText" + }, /*#__PURE__*/external_React_default().createElement("div", { + className: "weatherForecastRow" + }, /*#__PURE__*/external_React_default().createElement("span", { + className: "weatherTemperature" + }, WEATHER_SUGGESTION.current_conditions.temperature[this.props.Prefs.values["weather.temperatureUnits"]], "\xB0", this.props.Prefs.values["weather.temperatureUnits"])), /*#__PURE__*/external_React_default().createElement("div", { + className: "weatherCityRow" + }, /*#__PURE__*/external_React_default().createElement("span", { + className: "weatherCity" + }, WEATHER_SUGGESTION.city_name)), showDetailedView ? /*#__PURE__*/external_React_default().createElement("div", { + className: "weatherDetailedSummaryRow" + }, /*#__PURE__*/external_React_default().createElement("div", { + className: "weatherHighLowTemps" + }, /*#__PURE__*/external_React_default().createElement("span", null, WEATHER_SUGGESTION.forecast.high[this.props.Prefs.values["weather.temperatureUnits"]], "\xB0", this.props.Prefs.values["weather.temperatureUnits"]), /*#__PURE__*/external_React_default().createElement("span", null, "\u2022"), /*#__PURE__*/external_React_default().createElement("span", null, WEATHER_SUGGESTION.forecast.low[this.props.Prefs.values["weather.temperatureUnits"]], "\xB0", this.props.Prefs.values["weather.temperatureUnits"])), /*#__PURE__*/external_React_default().createElement("span", { + className: "weatherTextSummary" + }, WEATHER_SUGGESTION.current_conditions.summary)) : null)), /*#__PURE__*/external_React_default().createElement("div", { + className: "weatherButtonContextMenuWrapper" + }, /*#__PURE__*/external_React_default().createElement("button", { + "aria-haspopup": "true", + onKeyDown: this.onKeyDown, + onClick: this.onClick, + "data-l10n-id": "newtab-menu-section-tooltip", + className: "weatherButtonContextMenu" + }, showContextMenu ? /*#__PURE__*/external_React_default().createElement(LinkMenu, { + dispatch: dispatch, + index: index, + source: eventSource, + onUpdate: this.onUpdate, + options: WEATHER_SOURCE_CONTEXT_MENU_OPTIONS, + site: { + url: "https://support.mozilla.org/kb/customize-items-on-firefox-new-tab-page" + }, + link: "https://support.mozilla.org/kb/customize-items-on-firefox-new-tab-page", + shouldSendImpressionStats: shouldSendImpressionStats + }) : null))), /*#__PURE__*/external_React_default().createElement("span", { + "data-l10n-id": "newtab-weather-sponsored", + "data-l10n-args": "{\"provider\": \"AccuWeather\"}", + className: "weatherSponsorText" + })); + } + return /*#__PURE__*/external_React_default().createElement("div", { + ref: this.setErrorRef, + className: outerClassName + }, /*#__PURE__*/external_React_default().createElement("div", { + className: "weatherNotAvailable" + }, /*#__PURE__*/external_React_default().createElement("span", { + className: "icon icon-small-spacer icon-info-critical" + }), " ", /*#__PURE__*/external_React_default().createElement("span", { + "data-l10n-id": "newtab-weather-error-not-available" + }))); + } +} +const Weather_Weather = (0,external_ReactRedux_namespaceObject.connect)(state => ({ + Weather: state.Weather, + Prefs: state.Prefs, + IntersectionObserver: globalThis.IntersectionObserver, + document: globalThis.document +}))(_Weather); ;// CONCATENATED MODULE: ./content-src/components/Base/Base.jsx function Base_extends() { Base_extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return Base_extends.apply(this, arguments); } /* This Source Code Form is subject to the terms of the Mozilla Public @@ -9465,6 +9875,7 @@ function Base_extends() { Base_extends = Object.assign ? Object.assign.bind() : + const Base_VISIBLE = "visible"; const Base_VISIBILITY_CHANGE_EVENT = "visibilitychange"; const PrefsButton = ({ @@ -9660,7 +10071,7 @@ class BaseContent extends (external_React_default()).PureComponent { if (activeWallpaper && wallpaperList && name.url) { return /*#__PURE__*/external_React_default().createElement("p", { className: `wallpaper-attribution`, - key: name, + key: name.string, "data-l10n-id": "newtab-wallpaper-attribution", "data-l10n-args": JSON.stringify({ author_string: name.string, @@ -9688,6 +10099,14 @@ class BaseContent extends (external_React_default()).PureComponent { const darkWallpaper = wallpaperList.find(wp => wp.title === prefs["newtabWallpapers.wallpaper-dark"]) || ""; __webpack_require__.g.document?.body.style.setProperty(`--newtab-wallpaper-light`, `url(${lightWallpaper?.wallpaperUrl || ""})`); __webpack_require__.g.document?.body.style.setProperty(`--newtab-wallpaper-dark`, `url(${darkWallpaper?.wallpaperUrl || ""})`); + + // Add helper class to body if user has a wallpaper selected + if (lightWallpaper) { + __webpack_require__.g.document?.body.classList.add("hasWallpaperLight"); + } + if (darkWallpaper) { + __webpack_require__.g.document?.body.classList.add("hasWallpaperDark"); + } } } render() { @@ -9704,6 +10123,7 @@ class BaseContent extends (external_React_default()).PureComponent { const prefs = props.Prefs.values; const activeWallpaper = prefs[`newtabWallpapers.wallpaper-${this.state.colorMode}`]; const wallpapersEnabled = prefs["newtabWallpapers.enabled"]; + const weatherEnabled = prefs.showWeather; const { pocketConfig } = prefs; @@ -9723,10 +10143,12 @@ class BaseContent extends (external_React_default()).PureComponent { showSponsoredTopSitesEnabled: prefs.showSponsoredTopSites, showSponsoredPocketEnabled: prefs.showSponsored, showRecentSavesEnabled: prefs.showRecentSaves, - topSitesRowsCount: prefs.topSitesRows + topSitesRowsCount: prefs.topSitesRows, + weatherEnabled: prefs.showWeather }; const pocketRegion = prefs["feeds.system.topstories"]; const mayHaveSponsoredStories = prefs["system.showSponsored"]; + const mayHaveWeather = prefs["system.showWeather"]; const { mayHaveSponsoredTopSites } = prefs; @@ -9745,6 +10167,7 @@ class BaseContent extends (external_React_default()).PureComponent { pocketRegion: pocketRegion, mayHaveSponsoredTopSites: mayHaveSponsoredTopSites, mayHaveSponsoredStories: mayHaveSponsoredStories, + mayHaveWeather: mayHaveWeather, spocMessageVariant: spocMessageVariant, showing: customizeMenuVisible }), /*#__PURE__*/external_React_default().createElement("div", { @@ -9763,7 +10186,7 @@ class BaseContent extends (external_React_default()).PureComponent { locale: props.App.locale, mayHaveSponsoredStories: mayHaveSponsoredStories, firstVisibleTimestamp: this.state.firstVisibleTimestamp - })) : /*#__PURE__*/external_React_default().createElement(Sections_Sections, null)), /*#__PURE__*/external_React_default().createElement(ConfirmDialog, null), wallpapersEnabled && this.renderWallpaperAttribution()))); + })) : /*#__PURE__*/external_React_default().createElement(Sections_Sections, null)), /*#__PURE__*/external_React_default().createElement(ConfirmDialog, null), wallpapersEnabled && this.renderWallpaperAttribution()), /*#__PURE__*/external_React_default().createElement("aside", null, weatherEnabled && /*#__PURE__*/external_React_default().createElement(ErrorBoundary, null, /*#__PURE__*/external_React_default().createElement(Weather_Weather, null))))); } } BaseContent.defaultProps = { @@ -9775,7 +10198,8 @@ const Base = (0,external_ReactRedux_namespaceObject.connect)(state => ({ Sections: state.Sections, DiscoveryStream: state.DiscoveryStream, Search: state.Search, - Wallpapers: state.Wallpapers + Wallpapers: state.Wallpapers, + Weather: state.Weather }))(_Base); ;// CONCATENATED MODULE: ./content-src/lib/detect-user-session-start.mjs /* This Source Code Form is subject to the terms of the Mozilla Public diff --git a/browser/components/newtab/data/content/assets/glyph-info-critical-16.svg b/browser/components/newtab/data/content/assets/glyph-info-critical-16.svg new file mode 100644 index 0000000000..4fed88fb21 --- /dev/null +++ b/browser/components/newtab/data/content/assets/glyph-info-critical-16.svg @@ -0,0 +1,6 @@ +<!-- 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/. --> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="17" viewBox="0 0 16 17" fill="none"> + <path d="M7.625 16C6.64009 16 5.66482 15.806 4.75487 15.4291C3.84493 15.0522 3.01814 14.4997 2.3217 13.8033C1.62526 13.1069 1.07281 12.2801 0.695904 11.3701C0.318993 10.4602 0.125 9.48491 0.125 8.5C0.125 7.51509 0.318993 6.53982 0.695904 5.62987C1.07281 4.71993 1.62526 3.89314 2.3217 3.1967C3.01814 2.50026 3.84493 1.94781 4.75487 1.5709C5.66482 1.19399 6.64009 1 7.625 1C9.61412 1 11.5218 1.79018 12.9283 3.1967C14.3348 4.60322 15.125 6.51088 15.125 8.5C15.125 10.4891 14.3348 12.3968 12.9283 13.8033C11.5218 15.2098 9.61412 16 7.625 16ZM8.25 5.125C8.25 4.95924 8.18415 4.80027 8.06694 4.68306C7.94973 4.56585 7.79076 4.5 7.625 4.5C7.45924 4.5 7.30027 4.56585 7.18306 4.68306C7.06585 4.80027 7 4.95924 7 5.125V9.563C7 9.72876 7.06585 9.88773 7.18306 10.0049C7.30027 10.1222 7.45924 10.188 7.625 10.188C7.79076 10.188 7.94973 10.1222 8.06694 10.0049C8.18415 9.88773 8.25 9.72876 8.25 9.563V5.125ZM8.25 11.5L8 11.25H7.25L7 11.5V12.25L7.25 12.5H8L8.25 12.25V11.5Z" fill="context-fill"/> +</svg>
\ No newline at end of file diff --git a/browser/components/newtab/karma.mc.config.js b/browser/components/newtab/karma.mc.config.js index 886b19df7b..89887918e0 100644 --- a/browser/components/newtab/karma.mc.config.js +++ b/browser/components/newtab/karma.mc.config.js @@ -188,6 +188,15 @@ module.exports = function (config) { functions: 0, branches: 0, }, + /** + * Weather.jsx is tested via an xpcshell test + */ + "content-src/components/Weather/*.jsx": { + statements: 0, + lines: 0, + functions: 0, + branches: 0, + }, "content-src/components/DiscoveryStreamAdmin/*.jsx": { statements: 0, lines: 0, 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; +}; diff --git a/browser/components/newtab/metrics.yaml b/browser/components/newtab/metrics.yaml index c59247ceef..4a75cd65eb 100644 --- a/browser/components/newtab/metrics.yaml +++ b/browser/components/newtab/metrics.yaml @@ -233,6 +233,132 @@ newtab: send_in_pings: - newtab + wallpaper_click: + type: event + description: > + Recorded when a user clicks on a wallpaper option + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1896004 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1896004 + data_sensitivity: + - interaction + notification_emails: + - nbarrett@mozilla.com + expires: never + extra_keys: + selected_wallpaper: + description: > + Which wallpaper has been selected by the user + Will be the title of a Wallpaper or 'none' for users + that reset the background to default + type: string + had_previous_wallpaper: + description: > + Wheather or not user had a previously set wallpaper + type: boolean + newtab_visit_id: *newtab_visit_id + send_in_pings: + - newtab + + + weather_change_display: + type: event + description: > + Recorded when a user changes the weather display. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1895797 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1895797 + data_sensitivity: + - interaction + notification_emails: + - mcrawford@mozilla.com + expires: never + extra_keys: + weather_display_mode: &weather_display_mode + description: > + Which display mode is selected. + type: boolean + newtab_visit_id: *newtab_visit_id + send_in_pings: + - newtab + + weather_enabled: + lifetime: application + type: boolean + description: > + Whether the weather widget is enabled on the newtab. + Corresponds to the value of the + `browser.newtabpage.activity-stream.showWeather` pref. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1899340 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1899340 + data_sensitivity: + - interaction + notification_emails: + - mcrawford@mozilla.com + - sdowne@mozilla.com + expires: never + send_in_pings: + - newtab + + weather_open_provider_url: + type: event + description: > + Recorded when a user opens a link to the Weather provider website. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1895797 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1895797 + data_sensitivity: + - interaction + notification_emails: + - mcrawford@mozilla.com + expires: never + extra_keys: + newtab_visit_id: *newtab_visit_id + send_in_pings: + - newtab + + weather_impression: + type: event + description: > + Recorded when the weather widget is viewed + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1898275 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1895797 + data_sensitivity: + - interaction + notification_emails: + - mcrawford@mozilla.com + expires: never + extra_keys: + newtab_visit_id: *newtab_visit_id + send_in_pings: + - newtab + + weather_load_error: + type: event + description: > + Recorded when the weather widget is not available + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1898275 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1895797 + data_sensitivity: + - interaction + notification_emails: + - mcrawford@mozilla.com + expires: never + extra_keys: + newtab_visit_id: *newtab_visit_id + send_in_pings: + - newtab + + newtab.search: enabled: lifetime: application @@ -271,9 +397,10 @@ newtab.handoff_preference: - https://bugzilla.mozilla.org/show_bug.cgi?id=1864496 data_reviews: - https://bugzilla.mozilla.org/show_bug.cgi?id=1864496 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1892148 data_sensitivity: - interaction - expires: 128 + expires: 131 notification_emails: - fx-search-telemetry@mozilla.com diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_experiments_api_control.js b/browser/components/newtab/test/browser/abouthomecache/browser_experiments_api_control.js index a94f1fe055..0b49b3eb69 100644 --- a/browser/components/newtab/test/browser/abouthomecache/browser_experiments_api_control.js +++ b/browser/components/newtab/test/browser/abouthomecache/browser_experiments_api_control.js @@ -28,7 +28,9 @@ add_task(async function test_experiments_api_control() { }); Assert.ok( - !NimbusFeatures.abouthomecache.getVariable("enabled"), + !Services.prefs.getBoolPref( + "browser.startup.homepage.abouthome_cache.enabled" + ), "NimbusFeatures should tell us that the about:home startup cache " + "is disabled" ); @@ -51,7 +53,9 @@ add_task(async function test_experiments_api_control() { }); Assert.ok( - NimbusFeatures.abouthomecache.getVariable("enabled"), + Services.prefs.getBoolPref( + "browser.startup.homepage.abouthome_cache.enabled" + ), "NimbusFeatures should tell us that the about:home startup cache " + "is enabled" ); diff --git a/browser/components/newtab/test/browser/browser_customize_menu_content.js b/browser/components/newtab/test/browser/browser_customize_menu_content.js index ba83f1ff0a..fab032937a 100644 --- a/browser/components/newtab/test/browser/browser_customize_menu_content.js +++ b/browser/components/newtab/test/browser/browser_customize_menu_content.js @@ -1,5 +1,15 @@ "use strict"; +const { WeatherFeed } = ChromeUtils.importESModule( + "resource://activity-stream/lib/WeatherFeed.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + MerinoTestUtils: "resource://testing-common/MerinoTestUtils.sys.mjs", +}); + +const { WEATHER_SUGGESTION } = MerinoTestUtils; + test_newtab({ async before({ pushPrefs }) { await pushPrefs( @@ -118,6 +128,79 @@ test_newtab({ }); test_newtab({ + async before({ pushPrefs }) { + sinon.stub(WeatherFeed.prototype, "MerinoClient").returns({ + fetch: () => [WEATHER_SUGGESTION], + }); + await pushPrefs( + ["browser.newtabpage.activity-stream.system.showWeather", true], + ["browser.newtabpage.activity-stream.showWeather", false] + ); + }, + test: async function test_render_customizeMenuWeather() { + // Weather Widget Fecthing + function getWeatherWidget() { + return content.document.querySelector(`.weather`); + } + + function promiseWeatherShown() { + return ContentTaskUtils.waitForMutationCondition( + content.document.querySelector("aside"), + { childList: true, subtree: true }, + () => getWeatherWidget() + ); + } + + const WEATHER_PREF = "browser.newtabpage.activity-stream.showWeather"; + + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(".personalize-button"), + "Wait for prefs button to load on the newtab page" + ); + + let customizeButton = content.document.querySelector(".personalize-button"); + customizeButton.click(); + + let defaultPos = "matrix(1, 0, 0, 1, 0, 0)"; + await ContentTaskUtils.waitForCondition( + () => + content.getComputedStyle( + content.document.querySelector(".customize-menu") + ).transform === defaultPos, + "Customize Menu should be visible on screen" + ); + + // Test that clicking the weather toggle will make the + // weather widget appear on the newtab page. + // + // We waive XRay wrappers because we want to call the click() + // method defined on the toggle from this context. + let weatherSwitch = Cu.waiveXrays( + content.document.querySelector("#weather-section moz-toggle") + ); + Assert.ok( + !Services.prefs.getBoolPref(WEATHER_PREF), + "Weather pref is turned off" + ); + Assert.ok(!getWeatherWidget(), "Weather widget is not rendered"); + + let sectionShownPromise = promiseWeatherShown(); + weatherSwitch.click(); + await sectionShownPromise; + + Assert.ok(getWeatherWidget(), "Weather widget is rendered"); + }, + async after() { + Services.prefs.clearUserPref( + "browser.newtabpage.activity-stream.showWeather" + ); + Services.prefs.clearUserPref( + "browser.newtabpage.activity-stream.system.showWeather" + ); + }, +}); + +test_newtab({ test: async function test_open_close_customizeMenu() { const EventUtils = ContentTaskUtils.getEventUtils(content); await ContentTaskUtils.waitForCondition( diff --git a/browser/components/newtab/test/unit/content-src/components/CustomiseMenu.test.jsx b/browser/components/newtab/test/unit/content-src/components/CustomiseMenu.test.jsx index 0407622cf9..6186ca71fe 100644 --- a/browser/components/newtab/test/unit/content-src/components/CustomiseMenu.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/CustomiseMenu.test.jsx @@ -10,6 +10,7 @@ const DEFAULT_PROPS = { }, mayHaveSponsoredTopSites: true, mayHaveSponsoredStories: true, + mayHaveWeather: true, pocketRegion: true, dispatch: sinon.stub(), setPref: sinon.stub(), @@ -68,5 +69,9 @@ describe("ContentSection", () => { wrapper.find("#highlights-toggle").prop("data-eventSource"), "HIGHLIGHTS" ); + assert.equal( + wrapper.find("#weather-toggle").prop("data-eventSource"), + "WEATHER" + ); }); }); diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamAdmin.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamAdmin.test.jsx index 7f40b66200..006e83e663 100644 --- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamAdmin.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamAdmin.test.jsx @@ -67,6 +67,9 @@ describe("DiscoveryStreamAdmin", () => { otherPrefs={{}} state={{ DiscoveryStream: state, + Weather: { + suggestions: [], + }, }} /> ); @@ -90,7 +93,12 @@ describe("DiscoveryStreamAdmin", () => { wrapper = shallow( <DiscoveryStreamAdminUI otherPrefs={{}} - state={{ DiscoveryStream: state }} + state={{ + DiscoveryStream: state, + Weather: { + suggestions: [], + }, + }} /> ); wrapper.instance().onStoryToggle({ id: 12345 }); diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx index afb6d6dcd2..796f805444 100644 --- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx @@ -70,7 +70,9 @@ describe("<DSCard>", () => { }); it("should render DSLinkMenu", () => { - assert.equal(wrapper.children().at(3).type(), DSLinkMenu); + // Note: <DSLinkMenu> component moved from a direct child element of `.ds-card`. See Bug 1893936 + const default_link_menu = wrapper.find(DSLinkMenu); + assert.ok(default_link_menu.exists()); }); it("should start with no .active class", () => { diff --git a/browser/components/newtab/test/unit/lib/AboutPreferences.test.js b/browser/components/newtab/test/unit/lib/AboutPreferences.test.js index a19bf698d9..6555a1b77e 100644 --- a/browser/components/newtab/test/unit/lib/AboutPreferences.test.js +++ b/browser/components/newtab/test/unit/lib/AboutPreferences.test.js @@ -54,17 +54,21 @@ describe("AboutPreferences Feed", () => { instance.onAction(action); assert.calledOnce(action._target.browser.ownerGlobal.openPreferences); }); - it("should call .BrowserOpenAddonsMgr with the extension id on OPEN_WEBEXT_SETTINGS", () => { + it("should call .BrowserAddonUI.openAddonsMgr with the extension id on OPEN_WEBEXT_SETTINGS", () => { const action = { type: at.OPEN_WEBEXT_SETTINGS, data: "foo", _target: { - browser: { ownerGlobal: { BrowserOpenAddonsMgr: sinon.spy() } }, + browser: { + ownerGlobal: { + BrowserAddonUI: { openAddonsMgr: sinon.spy() }, + }, + }, }, }; instance.onAction(action); assert.calledWith( - action._target.browser.ownerGlobal.BrowserOpenAddonsMgr, + action._target.browser.ownerGlobal.BrowserAddonUI.openAddonsMgr, "addons://detail/foo" ); }); @@ -122,8 +126,9 @@ describe("AboutPreferences Feed", () => { const [, structure] = stub.firstCall.args; assert.equal(structure[0].id, "search"); assert.equal(structure[1].id, "topsites"); - assert.equal(structure[2].id, "topstories"); - assert.isEmpty(structure[2].rowsPref); + assert.equal(structure[2].id, "weather"); + assert.equal(structure[3].id, "topstories"); + assert.isEmpty(structure[3].rowsPref); }); }); describe("#renderPreferences", () => { diff --git a/browser/components/newtab/test/unit/lib/ActivityStream.test.js b/browser/components/newtab/test/unit/lib/ActivityStream.test.js index 7921ae2c91..ed00eb8202 100644 --- a/browser/components/newtab/test/unit/lib/ActivityStream.test.js +++ b/browser/components/newtab/test/unit/lib/ActivityStream.test.js @@ -311,6 +311,38 @@ describe("ActivityStream", () => { ); }); }); + describe("discoverystream.region-weather-config", () => { + let getVariableStub; + beforeEach(() => { + getVariableStub = sandbox.stub( + global.NimbusFeatures.pocketNewtab, + "getVariable" + ); + sandbox.stub(global.Region, "home").get(() => "CA"); + }); + it("should turn off weather system pref if no region weather config is set and no geo is set", () => { + getVariableStub.withArgs("regionWeatherConfig").returns(""); + sandbox.stub(global.Region, "home").get(() => ""); + + as._updateDynamicPrefs(); + + assert.isFalse(PREFS_CONFIG.get("system.showWeather").value); + }); + it("should turn on weather system pref based on region weather config pref", () => { + getVariableStub.withArgs("regionWeatherConfig").returns("CA"); + + as._updateDynamicPrefs(); + + assert.isTrue(PREFS_CONFIG.get("system.showWeather").value); + }); + it("should turn off weather system pref if no region weather config is set", () => { + getVariableStub.withArgs("regionWeatherConfig").returns(""); + + as._updateDynamicPrefs(); + + assert.isFalse(PREFS_CONFIG.get("system.showWeather").value); + }); + }); describe("_updateDynamicPrefs topstories default value", () => { let getVariableStub; let getBoolPrefStub; diff --git a/browser/components/newtab/test/unit/lib/ActivityStreamStorage.test.js b/browser/components/newtab/test/unit/lib/ActivityStreamStorage.test.js index 0b8baef762..fd56a3e185 100644 --- a/browser/components/newtab/test/unit/lib/ActivityStreamStorage.test.js +++ b/browser/components/newtab/test/unit/lib/ActivityStreamStorage.test.js @@ -47,6 +47,7 @@ describe("ActivityStreamStorage", () => { beforeEach(() => { storeStub = { getAll: sandbox.stub().resolves(), + getAllKeys: sandbox.stub().resolves(), get: sandbox.stub().resolves(), put: sandbox.stub().resolves(), }; @@ -75,6 +76,14 @@ describe("ActivityStreamStorage", () => { assert.calledOnce(storeStub.getAll); assert.deepEqual(result, ["bar"]); }); + it("should return the correct value for getAllKeys", async () => { + storeStub.getAllKeys.resolves(["key1", "key2", "key3"]); + + const result = await testStorage.getAllKeys(); + + assert.calledOnce(storeStub.getAllKeys); + assert.deepEqual(result, ["key1", "key2", "key3"]); + }); it("should query the correct object store", async () => { await testStorage.get(); diff --git a/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js b/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js index e10a4cbc04..72fc6bd0b8 100644 --- a/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js +++ b/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js @@ -3467,6 +3467,22 @@ describe("DiscoveryStreamFeed", () => { "https://bffApi/desktop/v1/recommendations?locale=$locale®ion=$region&count=30" ); }); + it("should update the new feed url with pocketFeedParameters", async () => { + globals.set("NimbusFeatures", { + pocketNewtab: { + getVariable: sandbox.stub(), + }, + }); + global.NimbusFeatures.pocketNewtab.getVariable + .withArgs("pocketFeedParameters") + .returns("&enableRankingByRegion=1"); + await feed.loadLayout(feed.store.dispatch); + const { layout } = feed.store.getState().DiscoveryStream; + assert.equal( + layout[0].components[2].feed.url, + "https://bffApi/desktop/v1/recommendations?locale=$locale®ion=$region&count=30&enableRankingByRegion=1" + ); + }); it("should fetch proper data from getComponentFeed", async () => { const fakeCache = {}; sandbox.stub(feed.cache, "get").returns(Promise.resolve(fakeCache)); diff --git a/browser/components/newtab/test/unit/lib/DownloadsManager.test.js b/browser/components/newtab/test/unit/lib/DownloadsManager.test.js index 5e2979893d..23ee9ffa34 100644 --- a/browser/components/newtab/test/unit/lib/DownloadsManager.test.js +++ b/browser/components/newtab/test/unit/lib/DownloadsManager.test.js @@ -29,6 +29,10 @@ describe("Downloads Manager", () => { showDownloadedFile: sinon.stub(), }); + globals.set("BrowserUtils", { + whereToOpenLink: sinon.stub().returns("current"), + }); + downloadsManager = new DownloadsManager(); downloadsManager.init({ dispatch() {} }); downloadsManager.onDownloadAdded({ diff --git a/browser/components/newtab/test/xpcshell/test_PlacesFeed.js b/browser/components/newtab/test/xpcshell/test_PlacesFeed.js index 78dda7818e..d6f7079d77 100644 --- a/browser/components/newtab/test/xpcshell/test_PlacesFeed.js +++ b/browser/components/newtab/test/xpcshell/test_PlacesFeed.js @@ -424,7 +424,7 @@ add_task(async function test_onAction_OPEN_LINK() { data: { url: "https://foo.com" }, _target: { browser: { - ownerGlobal: { openTrustedLinkIn, whereToOpenLink: () => "current" }, + ownerGlobal: { openTrustedLinkIn }, }, }, }; @@ -524,7 +524,7 @@ add_task(async function test_onAction_OPEN_LINK_pocket() { }, _target: { browser: { - ownerGlobal: { openTrustedLinkIn, whereToOpenLink: () => "current" }, + ownerGlobal: { openTrustedLinkIn }, }, }, }; @@ -551,7 +551,7 @@ add_task(async function test_onAction_OPEN_LINK_not_http() { data: { url: "file:///foo.com" }, _target: { browser: { - ownerGlobal: { openTrustedLinkIn, whereToOpenLink: () => "current" }, + ownerGlobal: { openTrustedLinkIn }, }, }, }; diff --git a/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js b/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js index 4be520fcca..247e08b333 100644 --- a/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js +++ b/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js @@ -2970,9 +2970,10 @@ add_task(async function test_ContileIntegration() { Assert.ok(fetched); // Both "foo" and "bar" should be filtered - Assert.equal(feed._contile.sites.length, 2); + Assert.equal(feed._contile.sites.length, 3); Assert.equal(feed._contile.sites[0].url, "https://www.test.com"); Assert.equal(feed._contile.sites[1].url, "https://test1.com"); + Assert.equal(feed._contile.sites[2].url, "https://test2.com"); } { diff --git a/browser/components/newtab/test/xpcshell/test_TopSitesFeed_glean.js b/browser/components/newtab/test/xpcshell/test_TopSitesFeed_glean.js index 5d13df0eb0..04501dbe53 100644 --- a/browser/components/newtab/test/xpcshell/test_TopSitesFeed_glean.js +++ b/browser/components/newtab/test/xpcshell/test_TopSitesFeed_glean.js @@ -47,6 +47,15 @@ let contileTile3 = { image_size: 200, impression_url: "https://impression_url.com", }; +let contileTile4 = { + id: 75899, + name: "Brand4", + url: "https://www.brand4.com", + click_url: "https://click_url.com", + image_url: "https://contile-images.jpg", + image_size: 200, + impression_url: "https://impression_url.com", +}; let mozSalesTile = [ { label: "MozSales Title", @@ -155,7 +164,12 @@ add_task(async function test_set_contile_tile_to_oversold() { let feed = getTopSitesFeedForTest(sandbox); feed._telemetryUtility.setSponsoredTilesConfigured(); - feed._telemetryUtility.setTiles([contileTile1, contileTile2, contileTile3]); + feed._telemetryUtility.setTiles([ + contileTile1, + contileTile2, + contileTile3, + contileTile4, + ]); let mergedTiles = [ { @@ -170,12 +184,18 @@ add_task(async function test_set_contile_tile_to_oversold() { sponsored_position: 2, partner: "amp", }, + { + url: "https://www.brand3.com", + label: "brand3", + sponsored_position: 3, + partner: "amp", + }, ]; feed._telemetryUtility.determineFilteredTilesAndSetToOversold(mergedTiles); feed._telemetryUtility.finalizeNewtabPingFields(mergedTiles); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); let expectedResult = { sponsoredTilesReceived: [ @@ -194,6 +214,12 @@ add_task(async function test_set_contile_tile_to_oversold() { { advertiser: "brand3", provider: "amp", + display_position: 3, + display_fail_reason: null, + }, + { + advertiser: "brand4", + provider: "amp", display_position: null, display_fail_reason: "oversold", }, @@ -477,7 +503,12 @@ add_task(async function test_set_tiles_to_dismissed_then_updated() { feed._telemetryUtility.setSponsoredTilesConfigured(); // Step 1: Set initial tiles - feed._telemetryUtility.setTiles([contileTile1, contileTile2, contileTile3]); + feed._telemetryUtility.setTiles([ + contileTile1, + contileTile2, + contileTile3, + contileTile4, + ]); // Step 2: Set all tiles to dismissed feed._telemetryUtility.determineFilteredTilesAndSetToDismissed([]); @@ -495,12 +526,18 @@ add_task(async function test_set_tiles_to_dismissed_then_updated() { sponsored_position: 2, partner: "amp", }, + { + url: "https://www.brand3.com", + label: "brand3", + sponsored_position: 3, + partner: "amp", + }, ]; // Step 3: Finalize with the updated list of tiles. feed._telemetryUtility.finalizeNewtabPingFields(updatedTiles); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); let expectedResult = { sponsoredTilesReceived: [ @@ -522,6 +559,12 @@ add_task(async function test_set_tiles_to_dismissed_then_updated() { display_position: null, display_fail_reason: "dismissed", }, + { + advertiser: "brand4", + provider: "amp", + display_position: null, + display_fail_reason: "dismissed", + }, ], }; Assert.equal( @@ -537,7 +580,12 @@ add_task(async function test_set_tile_positions_after_updated_list() { feed._telemetryUtility.setSponsoredTilesConfigured(); // Step 1: Set initial tiles - feed._telemetryUtility.setTiles([contileTile1, contileTile2, contileTile3]); + feed._telemetryUtility.setTiles([ + contileTile1, + contileTile2, + contileTile3, + contileTile4, + ]); // Step 2: Set 1 tile to oversold (brand3) let mergedTiles = [ @@ -553,6 +601,12 @@ add_task(async function test_set_tile_positions_after_updated_list() { sponsored_position: 2, partner: "amp", }, + { + url: "https://www.brand3.com", + label: "brand3", + sponsored_position: 3, + partner: "amp", + }, ]; feed._telemetryUtility.determineFilteredTilesAndSetToOversold(mergedTiles); @@ -570,10 +624,16 @@ add_task(async function test_set_tile_positions_after_updated_list() { sponsored_position: 2, partner: "amp", }, + { + url: "https://www.brand3.com", + label: "brand3", + sponsored_position: 3, + partner: "amp", + }, ]; feed._telemetryUtility.finalizeNewtabPingFields(updatedTiles); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); let expectedResult = { sponsoredTilesReceived: [ @@ -592,6 +652,12 @@ add_task(async function test_set_tile_positions_after_updated_list() { { advertiser: "brand3", provider: "amp", + display_position: 3, + display_fail_reason: null, + }, + { + advertiser: "brand4", + provider: "amp", display_position: null, display_fail_reason: "oversold", }, @@ -610,7 +676,12 @@ add_task(async function test_set_tile_positions_after_updated_list_all_tiles() { feed._telemetryUtility.setSponsoredTilesConfigured(); // Step 1: Set initial tiles - feed._telemetryUtility.setTiles([contileTile1, contileTile2, contileTile3]); + feed._telemetryUtility.setTiles([ + contileTile1, + contileTile2, + contileTile3, + contileTile4, + ]); // Step 2: Set 1 tile to oversold (brand3) let mergedTiles = [ @@ -626,6 +697,12 @@ add_task(async function test_set_tile_positions_after_updated_list_all_tiles() { sponsored_position: 2, partner: "amp", }, + { + url: "https://www.brand3.com", + label: "brand3", + sponsored_position: 3, + partner: "amp", + }, ]; feed._telemetryUtility.determineFilteredTilesAndSetToOversold(mergedTiles); @@ -643,10 +720,16 @@ add_task(async function test_set_tile_positions_after_updated_list_all_tiles() { sponsored_position: 2, partner: "amp", }, + { + url: "https://www.replacement3.com", + label: "replacement3", + sponsored_position: 3, + partner: "amp", + }, ]; feed._telemetryUtility.finalizeNewtabPingFields(updatedTiles); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); let expectedResult = { sponsoredTilesReceived: [ @@ -665,6 +748,12 @@ add_task(async function test_set_tile_positions_after_updated_list_all_tiles() { { advertiser: "brand3", provider: "amp", + display_position: 3, + display_fail_reason: null, + }, + { + advertiser: "brand4", + provider: "amp", display_position: null, display_fail_reason: "oversold", }, @@ -684,7 +773,12 @@ add_task( feed._telemetryUtility.setSponsoredTilesConfigured(); // Step 1: Set initial tiles - feed._telemetryUtility.setTiles([contileTile1, contileTile2, contileTile3]); + feed._telemetryUtility.setTiles([ + contileTile1, + contileTile2, + contileTile3, + contileTile4, + ]); // Step 2: Set 1 tile to oversold (brand3) let mergedTiles = [ @@ -700,6 +794,12 @@ add_task( sponsored_position: 2, partner: "amp", }, + { + url: "https://www.brand3.com", + label: "brand3", + sponsored_position: 3, + partner: "amp", + }, ]; feed._telemetryUtility.determineFilteredTilesAndSetToOversold(mergedTiles); @@ -717,10 +817,16 @@ add_task( sponsored_position: 2, partner: "amp", }, + { + url: "https://www.brand3.com", + label: "brand3", + sponsored_position: 3, + partner: "amp", + }, ]; feed._telemetryUtility.finalizeNewtabPingFields(updatedTiles); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); let expectedResult = { sponsoredTilesReceived: [ @@ -739,6 +845,12 @@ add_task( { advertiser: "brand3", provider: "amp", + display_position: 3, + display_fail_reason: null, + }, + { + advertiser: "brand4", + provider: "amp", display_position: null, display_fail_reason: "oversold", }, @@ -901,7 +1013,7 @@ add_task(async function test_all_tiles_displayed() { await feed._readDefaults(); await feed.getLinksWithDefaults(false); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); let expectedResult = { sponsoredTilesReceived: [ @@ -961,18 +1073,25 @@ add_task(async function test_set_one_tile_display_fail_reason_to_oversold() { impression_url: "https://www.brand3-impression.com", name: "brand3", }, + { + url: "https://www.brand4.com", + image_url: "images/brnad4-com.png", + click_url: "https://www.brand4-click.com", + impression_url: "https://www.brand4-impression.com", + name: "brand4", + }, ], }), }); const fetched = await feed._contile._fetchSites(); Assert.ok(fetched); - Assert.equal(feed._contile.sites.length, 2); + Assert.equal(feed._contile.sites.length, 3); await feed._readDefaults(); await feed.getLinksWithDefaults(false); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); let expectedResult = { sponsoredTilesReceived: [ { @@ -990,6 +1109,12 @@ add_task(async function test_set_one_tile_display_fail_reason_to_oversold() { { advertiser: "brand3", provider: "amp", + display_position: 3, + display_fail_reason: null, + }, + { + advertiser: "brand4", + provider: "amp", display_position: null, display_fail_reason: "oversold", }, @@ -1052,7 +1177,7 @@ add_task(async function test_set_one_tile_display_fail_reason_to_dismissed() { await feed._readDefaults(); await feed.getLinksWithDefaults(false); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); let expectedResult = { sponsoredTilesReceived: [ @@ -1129,6 +1254,13 @@ add_task( impression_url: "https://www.brand4-impression.com", name: "brand4", }, + { + url: "https://www.brand5.com", + image_url: "images/brand5-com.png", + click_url: "https://www.brand5-click.com", + impression_url: "https://www.brand5-impression.com", + name: "brand5", + }, ], }), }); @@ -1140,12 +1272,12 @@ add_task( const fetched = await feed._contile._fetchSites(); Assert.ok(fetched); - Assert.equal(feed._contile.sites.length, 2); + Assert.equal(feed._contile.sites.length, 3); await feed._readDefaults(); await feed.getLinksWithDefaults(false); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); let expectedResult = { sponsoredTilesReceived: [ @@ -1170,6 +1302,12 @@ add_task( { advertiser: "brand4", provider: "amp", + display_position: 3, + display_fail_reason: null, + }, + { + advertiser: "brand5", + provider: "amp", display_position: null, display_fail_reason: "oversold", }, @@ -1233,7 +1371,7 @@ add_task( await feed._readDefaults(); await feed.getLinksWithDefaults(false); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); let expectedResult = { sponsoredTilesReceived: [ @@ -1297,6 +1435,13 @@ add_task(async function test_update_tile_count() { impression_url: "https://www.brand3-impression.com", name: "brand3", }, + { + url: "https://www.brand4.com", + image_url: "images/brand4-com.png", + click_url: "https://www.brand4-click.com", + impression_url: "https://www.brand4-impression.com", + name: "brand4", + }, ], }), }); @@ -1304,11 +1449,11 @@ add_task(async function test_update_tile_count() { // 1. Initially the Nimbus pref is set to 2 tiles let fetched = await feed._contile._fetchSites(); Assert.ok(fetched); - Assert.equal(feed._contile.sites.length, 2); + Assert.equal(feed._contile.sites.length, 3); await feed._readDefaults(); await feed.getLinksWithDefaults(false); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); let expectedResult = { sponsoredTilesReceived: [ @@ -1327,6 +1472,12 @@ add_task(async function test_update_tile_count() { { advertiser: "brand3", provider: "amp", + display_position: 3, + display_fail_reason: null, + }, + { + advertiser: "brand4", + provider: "amp", display_position: null, display_fail_reason: "oversold", }, @@ -1344,7 +1495,7 @@ add_task(async function test_update_tile_count() { ); setNimbusVariablesForNumTiles(nimbusPocketStub, 3); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); expectedResult = { sponsoredTilesReceived: [ @@ -1363,6 +1514,12 @@ add_task(async function test_update_tile_count() { { advertiser: "brand3", provider: "amp", + display_position: 3, + display_fail_reason: null, + }, + { + advertiser: "brand4", + provider: "amp", display_position: null, display_fail_reason: "oversold", }, @@ -1402,6 +1559,12 @@ add_task(async function test_update_tile_count() { display_position: 3, display_fail_reason: null, }, + { + advertiser: "brand4", + provider: "amp", + display_position: null, + display_fail_reason: "oversold", + }, ], }; Assert.equal( @@ -1442,6 +1605,13 @@ add_task(async function test_update_tile_count_sourced_from_cache() { impression_url: "https://www.brand3-impression.com", name: "brand3", }, + { + url: "https://www.brand4.com", + image_url: "images/brand4-com.png", + click_url: "https://www.brand4-click.com", + impression_url: "https://www.brand4-impression.com", + name: "brand4", + }, ]; Services.prefs.setStringPref(CONTILE_CACHE_PREF, JSON.stringify(tiles)); @@ -1459,11 +1629,11 @@ add_task(async function test_update_tile_count_sourced_from_cache() { // Ensure ContileIntegration._fetchSites is working populate _sites and initilize TelemetryUtility let fetched = await feed._contile._fetchSites(); Assert.ok(fetched); - Assert.equal(feed._contile.sites.length, 3); + Assert.equal(feed._contile.sites.length, 4); await feed._readDefaults(); await feed.getLinksWithDefaults(false); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); let expectedResult = { sponsoredTilesReceived: [ @@ -1482,6 +1652,12 @@ add_task(async function test_update_tile_count_sourced_from_cache() { { advertiser: "brand3", provider: "amp", + display_position: 3, + display_fail_reason: null, + }, + { + advertiser: "brand4", + provider: "amp", display_position: null, display_fail_reason: "oversold", }, @@ -1499,12 +1675,12 @@ add_task(async function test_update_tile_count_sourced_from_cache() { ); setNimbusVariablesForNumTiles(nimbusPocketStub, 3); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); // 3. Confirm the new count is applied when data pulled from Contile, 3 tiles displayed fetched = await feed._contile._fetchSites(); Assert.ok(fetched); - Assert.equal(feed._contile.sites.length, 3); + Assert.equal(feed._contile.sites.length, 4); await feed._readDefaults(); await feed.getLinksWithDefaults(false); @@ -1530,6 +1706,12 @@ add_task(async function test_update_tile_count_sourced_from_cache() { display_position: 3, display_fail_reason: null, }, + { + advertiser: "brand4", + provider: "amp", + display_position: null, + display_fail_reason: "oversold", + }, ], }; Assert.equal( @@ -1595,7 +1777,7 @@ add_task( await feed._readDefaults(); await feed.getLinksWithDefaults(false); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); let expectedResult = { sponsoredTilesReceived: [ @@ -1636,7 +1818,7 @@ add_task( await feed._readDefaults(); await feed.getLinksWithDefaults(false); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); expectedResult = { sponsoredTilesReceived: [ @@ -1684,7 +1866,7 @@ add_task(async function test_sponsoredTilesReceived_not_set() { await feed._readDefaults(); await feed.getLinksWithDefaults(false); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); let expectedResult = { sponsoredTilesReceived: [] }; Assert.equal( @@ -1744,7 +1926,7 @@ add_task(async function test_telemetry_data_updates() { await feed._readDefaults(); await feed.getLinksWithDefaults(false); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); let expectedResult = { sponsoredTilesReceived: [ @@ -1896,7 +2078,7 @@ add_task(async function test_reset_telemetry_data() { await feed._readDefaults(); await feed.getLinksWithDefaults(false); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); let expectedResult = { sponsoredTilesReceived: [ @@ -1934,7 +2116,7 @@ add_task(async function test_reset_telemetry_data() { await feed._readDefaults(); await feed.getLinksWithDefaults(false); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); expectedResult = { sponsoredTilesReceived: [] }; Assert.equal( @@ -1982,17 +2164,24 @@ add_task(async function test_set_telemetry_for_moz_sales_tiles() { impression_url: "https://www.brand2-impression.com", name: "brand2", }, + { + url: "https://www.brand3.com", + image_url: "images/brand3-com.png", + click_url: "https://www.brand3-click.com", + impression_url: "https://www.brand3-impression.com", + name: "brand2", + }, ], }), }); const fetched = await feed._contile._fetchSites(); Assert.ok(fetched); - Assert.equal(feed._contile.sites.length, 2); + Assert.equal(feed._contile.sites.length, 3); await feed._readDefaults(); await feed.getLinksWithDefaults(false); - Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2); + Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3); let expectedResult = { sponsoredTilesReceived: [ { @@ -2008,6 +2197,12 @@ add_task(async function test_set_telemetry_for_moz_sales_tiles() { display_fail_reason: null, }, { + advertiser: "brand3", + provider: "amp", + display_position: 3, + display_fail_reason: null, + }, + { advertiser: "mozsales title", provider: "moz-sales", display_position: null, diff --git a/browser/components/newtab/test/xpcshell/test_WeatherFeed.js b/browser/components/newtab/test/xpcshell/test_WeatherFeed.js new file mode 100644 index 0000000000..2821f4b7d0 --- /dev/null +++ b/browser/components/newtab/test/xpcshell/test_WeatherFeed.js @@ -0,0 +1,99 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { WeatherFeed } = ChromeUtils.importESModule( + "resource://activity-stream/lib/WeatherFeed.sys.mjs" +); + +const { actionCreators: ac, actionTypes: at } = ChromeUtils.importESModule( + "resource://activity-stream/common/Actions.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + sinon: "resource://testing-common/Sinon.sys.mjs", + MerinoTestUtils: "resource://testing-common/MerinoTestUtils.sys.mjs", +}); + +const { WEATHER_SUGGESTION } = MerinoTestUtils; + +const WEATHER_ENABLED = "browser.newtabpage.activity-stream.showWeather"; +const SYS_WEATHER_ENABLED = + "browser.newtabpage.activity-stream.system.showWeather"; + +add_task(async function test_construction() { + let sandbox = sinon.createSandbox(); + sandbox.stub(WeatherFeed.prototype, "PersistentCache").returns({ + set: () => {}, + get: () => {}, + }); + + let feed = new WeatherFeed(); + + info("WeatherFeed constructor should create initial values"); + + Assert.ok(feed, "Could construct a WeatherFeed"); + Assert.ok(feed.loaded === false, "WeatherFeed is not loaded"); + Assert.ok(feed.merino === null, "merino is initialized as null"); + Assert.ok( + feed.suggestions.length === 0, + "suggestions is initialized as a array with length of 0" + ); + Assert.ok(feed.fetchTimer === null, "fetchTimer is initialized as null"); + sandbox.restore(); +}); + +add_task(async function test_onAction_INIT() { + let sandbox = sinon.createSandbox(); + sandbox.stub(WeatherFeed.prototype, "MerinoClient").returns({ + get: () => [WEATHER_SUGGESTION], + on: () => {}, + }); + sandbox.stub(WeatherFeed.prototype, "PersistentCache").returns({ + set: () => {}, + get: () => {}, + }); + const dateNowTestValue = 1; + sandbox.stub(WeatherFeed.prototype, "Date").returns({ + now: () => dateNowTestValue, + }); + + let feed = new WeatherFeed(); + + Services.prefs.setBoolPref(WEATHER_ENABLED, true); + Services.prefs.setBoolPref(SYS_WEATHER_ENABLED, true); + + sandbox.stub(feed, "isEnabled").returns(true); + + sandbox.stub(feed, "fetchHelper"); + feed.suggestions = [WEATHER_SUGGESTION]; + + feed.store = { + dispatch: sinon.spy(), + }; + + info("WeatherFeed.onAction INIT should initialize Weather"); + + await feed.onAction({ + type: at.INIT, + }); + + Assert.ok(feed.store.dispatch.calledOnce); + Assert.ok( + feed.store.dispatch.calledWith( + ac.BroadcastToContent({ + type: at.WEATHER_UPDATE, + data: { + suggestions: [WEATHER_SUGGESTION], + lastUpdated: dateNowTestValue, + }, + meta: { + isStartup: true, + }, + }) + ) + ); + Services.prefs.clearUserPref(WEATHER_ENABLED); + sandbox.restore(); +}); diff --git a/browser/components/newtab/test/xpcshell/xpcshell.toml b/browser/components/newtab/test/xpcshell/xpcshell.toml index 13c11b0541..567927c31c 100644 --- a/browser/components/newtab/test/xpcshell/xpcshell.toml +++ b/browser/components/newtab/test/xpcshell/xpcshell.toml @@ -28,3 +28,5 @@ support-files = ["../schemas/*.schema.json"] ["test_TopSitesFeed_glean.js"] ["test_WallpaperFeed.js"] + +["test_WeatherFeed.js"] |