diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:42 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:42 +0000 |
commit | da4c7e7ed675c3bf405668739c3012d140856109 (patch) | |
tree | cdd868dba063fecba609a1d819de271f0d51b23e /browser/components/newtab | |
parent | Adding upstream version 125.0.3. (diff) | |
download | firefox-da4c7e7ed675c3bf405668739c3012d140856109.tar.xz firefox-da4c7e7ed675c3bf405668739c3012d140856109.zip |
Adding upstream version 126.0.upstream/126.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/newtab')
128 files changed, 2244 insertions, 594 deletions
diff --git a/browser/components/newtab/.eslintrc.js b/browser/components/newtab/.eslintrc.js index f541cdd988..29114a055a 100644 --- a/browser/components/newtab/.eslintrc.js +++ b/browser/components/newtab/.eslintrc.js @@ -15,11 +15,7 @@ module.exports = { { // TODO: Bug 1773467 - Move these to .mjs or figure out a generic way // to identify these as modules. - files: [ - "content-src/**/*.js", - "test/schemas/**/*.js", - "test/unit/**/*.js", - ], + files: ["test/schemas/**/*.js", "test/unit/**/*.js"], parserOptions: { sourceType: "module", }, @@ -92,8 +88,6 @@ module.exports = { }, ], rules: { - "fetch-options/no-fetch-credentials": "error", - "react/jsx-boolean-value": ["error", "always"], "react/jsx-key": "error", "react/jsx-no-bind": [ diff --git a/browser/components/newtab/common/Actions.sys.mjs b/browser/components/newtab/common/Actions.mjs index df5c9f0c91..7273d80220 100644 --- a/browser/components/newtab/common/Actions.sys.mjs +++ b/browser/components/newtab/common/Actions.mjs @@ -2,6 +2,8 @@ * 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/. */ +// This file is accessed from both content and system scopes. + export const MAIN_MESSAGE_TYPE = "ActivityStream:Main"; export const CONTENT_MESSAGE_TYPE = "ActivityStream:Content"; export const PRELOAD_MESSAGE_TYPE = "ActivityStream:PreloadedBrowser"; @@ -158,6 +160,7 @@ for (const type of [ "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", + "WALLPAPERS_SET", "WEBEXT_CLICK", "WEBEXT_DISMISS", ]) { @@ -371,8 +374,11 @@ function DiscoveryStreamLoadedContent( return importContext === UI_CODE ? AlsoToMain(action) : action; } -function SetPref(name, value, importContext = globalImportContext) { - const action = { type: actionTypes.SET_PREF, data: { name, value } }; +function SetPref(prefName, value, importContext = globalImportContext) { + const action = { + type: actionTypes.SET_PREF, + data: { name: prefName, value }, + }; return importContext === UI_CODE ? AlsoToMain(action) : action; } diff --git a/browser/components/newtab/common/Reducers.sys.mjs b/browser/components/newtab/common/Reducers.sys.mjs index d4f879b834..326217538d 100644 --- a/browser/components/newtab/common/Reducers.sys.mjs +++ b/browser/components/newtab/common/Reducers.sys.mjs @@ -2,7 +2,7 @@ * 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 { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs"; +import { actionTypes as at } from "resource://activity-stream/common/Actions.mjs"; import { Dedupe } from "resource://activity-stream/common/Dedupe.sys.mjs"; export const TOP_SITES_DEFAULT_ROWS = 1; @@ -101,6 +101,9 @@ export const INITIAL_STATE = { // Hide the search box after handing off to AwesomeBar and user starts typing. hide: false, }, + Wallpapers: { + wallpaperList: [], + }, }; function App(prevState = INITIAL_STATE.App, action) { @@ -841,6 +844,15 @@ function Search(prevState = INITIAL_STATE.Search, action) { } } +function Wallpapers(prevState = INITIAL_STATE.Wallpapers, action) { + switch (action.type) { + case at.WALLPAPERS_SET: + return { wallpaperList: action.data }; + default: + return prevState; + } +} + export const reducers = { TopSites, App, @@ -852,4 +864,5 @@ export const reducers = { Personalization, DiscoveryStream, Search, + Wallpapers, }; diff --git a/browser/components/newtab/content-src/activity-stream.jsx b/browser/components/newtab/content-src/activity-stream.jsx index c588e8e850..57ba9f9c92 100644 --- a/browser/components/newtab/content-src/activity-stream.jsx +++ b/browser/components/newtab/content-src/activity-stream.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { Base } from "content-src/components/Base/Base"; import { DetectUserSessionStart } from "content-src/lib/detect-user-session-start"; import { initStore } from "content-src/lib/init-store"; diff --git a/browser/components/newtab/content-src/components/Base/Base.jsx b/browser/components/newtab/content-src/components/Base/Base.jsx index 20402b09f5..1738f8f51a 100644 --- a/browser/components/newtab/content-src/components/Base/Base.jsx +++ b/browser/components/newtab/content-src/components/Base/Base.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { DiscoveryStreamAdmin } from "content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin"; import { ConfirmDialog } from "content-src/components/ConfirmDialog/ConfirmDialog"; import { connect } from "react-redux"; @@ -16,6 +13,9 @@ import React from "react"; import { Search } from "content-src/components/Search/Search"; import { Sections } from "content-src/components/Sections/Sections"; +const VISIBLE = "visible"; +const VISIBILITY_CHANGE_EVENT = "visibilitychange"; + export const PrefsButton = ({ onClick, icon }) => ( <div className="prefs-button"> <button @@ -76,7 +76,7 @@ export class _Base extends React.PureComponent { ] .filter(v => v) .join(" "); - global.document.body.className = bodyClassName; + globalThis.document.body.className = bodyClassName; } render() { @@ -110,17 +110,75 @@ export class BaseContent extends React.PureComponent { this.handleOnKeyDown = this.handleOnKeyDown.bind(this); this.onWindowScroll = debounce(this.onWindowScroll.bind(this), 5); this.setPref = this.setPref.bind(this); - this.state = { fixedSearch: false }; + this.updateWallpaper = this.updateWallpaper.bind(this); + this.prefersDarkQuery = null; + this.handleColorModeChange = this.handleColorModeChange.bind(this); + this.state = { + fixedSearch: false, + firstVisibleTimestamp: null, + colorMode: "", + }; + } + + setFirstVisibleTimestamp() { + if (!this.state.firstVisibleTimestamp) { + this.setState({ + firstVisibleTimestamp: Date.now(), + }); + } } componentDidMount() { global.addEventListener("scroll", this.onWindowScroll); global.addEventListener("keydown", this.handleOnKeyDown); + if (this.props.document.visibilityState === VISIBLE) { + this.setFirstVisibleTimestamp(); + } else { + this._onVisibilityChange = () => { + if (this.props.document.visibilityState === VISIBLE) { + this.setFirstVisibleTimestamp(); + this.props.document.removeEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + this._onVisibilityChange = null; + } + }; + this.props.document.addEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + // track change event to dark/light mode + this.prefersDarkQuery = globalThis.matchMedia( + "(prefers-color-scheme: dark)" + ); + + this.prefersDarkQuery.addEventListener( + "change", + this.handleColorModeChange + ); + this.handleColorModeChange(); + } + + handleColorModeChange() { + const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; + this.setState({ colorMode }); } componentWillUnmount() { + this.prefersDarkQuery?.removeEventListener( + "change", + this.handleColorModeChange + ); global.removeEventListener("scroll", this.onWindowScroll); global.removeEventListener("keydown", this.handleOnKeyDown); + if (this._onVisibilityChange) { + this.props.document.removeEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } } onWindowScroll() { @@ -160,11 +218,79 @@ export class BaseContent extends React.PureComponent { this.props.dispatch(ac.SetPref(pref, value)); } + renderWallpaperAttribution() { + const { wallpaperList } = this.props.Wallpapers; + const activeWallpaper = + this.props.Prefs.values[ + `newtabWallpapers.wallpaper-${this.state.colorMode}` + ]; + const selected = wallpaperList.find(wp => wp.title === activeWallpaper); + // make sure a wallpaper is selected and that the attribution also exists + if (!selected?.attribution) { + return null; + } + + const { name, webpage } = selected.attribution; + if (activeWallpaper && wallpaperList && name.url) { + return ( + <p + className={`wallpaper-attribution`} + key={name} + data-l10n-id="newtab-wallpaper-attribution" + data-l10n-args={JSON.stringify({ + author_string: name.string, + author_url: name.url, + webpage_string: webpage.string, + webpage_url: webpage.url, + })} + > + <a data-l10n-name="name-link" href={name.url}> + {name.string} + </a> + <a data-l10n-name="webpage-link" href={webpage.url}> + {webpage.string} + </a> + </p> + ); + } + return null; + } + + async updateWallpaper() { + const prefs = this.props.Prefs.values; + const { wallpaperList } = this.props.Wallpapers; + + if (wallpaperList) { + const lightWallpaper = + wallpaperList.find( + wp => wp.title === prefs["newtabWallpapers.wallpaper-light"] + ) || ""; + const darkWallpaper = + wallpaperList.find( + wp => wp.title === prefs["newtabWallpapers.wallpaper-dark"] + ) || ""; + global.document?.body.style.setProperty( + `--newtab-wallpaper-light`, + `url(${lightWallpaper?.wallpaperUrl || ""})` + ); + + global.document?.body.style.setProperty( + `--newtab-wallpaper-dark`, + `url(${darkWallpaper?.wallpaperUrl || ""})` + ); + } + } + render() { const { props } = this; const { App } = props; const { initialized, customizeMenuVisible } = App; const prefs = props.Prefs.values; + + const activeWallpaper = + prefs[`newtabWallpapers.wallpaper-${this.state.colorMode}`]; + const wallpapersEnabled = prefs["newtabWallpapers.enabled"]; + const { pocketConfig } = prefs; const isDiscoveryStream = @@ -215,6 +341,9 @@ export class BaseContent extends React.PureComponent { ] .filter(v => v) .join(" "); + if (wallpapersEnabled) { + this.updateWallpaper(); + } return ( <div> @@ -224,6 +353,8 @@ export class BaseContent extends React.PureComponent { openPreferences={this.openPreferences} setPref={this.setPref} enabledSections={enabledSections} + wallpapersEnabled={wallpapersEnabled} + activeWallpaper={activeWallpaper} pocketRegion={pocketRegion} mayHaveSponsoredTopSites={mayHaveSponsoredTopSites} mayHaveSponsoredStories={mayHaveSponsoredStories} @@ -252,6 +383,7 @@ export class BaseContent extends React.PureComponent { <DiscoveryStreamBase locale={props.App.locale} mayHaveSponsoredStories={mayHaveSponsoredStories} + firstVisibleTimestamp={this.state.firstVisibleTimestamp} /> </ErrorBoundary> ) : ( @@ -259,6 +391,7 @@ export class BaseContent extends React.PureComponent { )} </div> <ConfirmDialog /> + {wallpapersEnabled && this.renderWallpaperAttribution()} </main> </div> </div> @@ -266,10 +399,15 @@ export class BaseContent extends React.PureComponent { } } +BaseContent.defaultProps = { + document: global.document, +}; + export const Base = connect(state => ({ App: state.App, Prefs: state.Prefs, Sections: state.Sections, DiscoveryStream: state.DiscoveryStream, Search: state.Search, + Wallpapers: state.Wallpapers, }))(_Base); diff --git a/browser/components/newtab/content-src/components/Base/_Base.scss b/browser/components/newtab/content-src/components/Base/_Base.scss index 1282173df5..a9141e0923 100644 --- a/browser/components/newtab/content-src/components/Base/_Base.scss +++ b/browser/components/newtab/content-src/components/Base/_Base.scss @@ -24,10 +24,17 @@ } main { - margin: auto; + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; width: $wrapper-default-width; padding: 0; + .vertical-center-wrapper { + margin: auto 0; + } + section { margin-bottom: $section-spacing; position: relative; @@ -124,3 +131,32 @@ main { } } } + +.wallpaper-attribution { + padding: 0 $section-horizontal-padding; + font-size: 14px; + + &.theme-light { + display: inline-block; + + @include dark-theme-only { + display: none; + } + } + + &.theme-dark { + display: none; + + @include dark-theme-only { + display: inline-block; + } + } + + a { + color: var(--newtab-element-color); + + &:hover { + text-decoration: none; + } + } +} diff --git a/browser/components/newtab/content-src/components/Card/Card.jsx b/browser/components/newtab/content-src/components/Card/Card.jsx index 9d03377f1b..da5e0346d7 100644 --- a/browser/components/newtab/content-src/components/Card/Card.jsx +++ b/browser/components/newtab/content-src/components/Card/Card.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { cardContextTypes } from "./types"; import { connect } from "react-redux"; import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton"; diff --git a/browser/components/newtab/content-src/components/Card/types.js b/browser/components/newtab/content-src/components/Card/types.mjs index 0b17eea408..0b17eea408 100644 --- a/browser/components/newtab/content-src/components/Card/types.js +++ b/browser/components/newtab/content-src/components/Card/types.mjs diff --git a/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx b/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx index 98bf88fbea..2046617ad6 100644 --- a/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx +++ b/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx @@ -119,7 +119,7 @@ export class _CollapsibleSection extends React.PureComponent { } _CollapsibleSection.defaultProps = { - document: global.document || { + document: globalThis.document || { addEventListener: () => {}, removeEventListener: () => {}, visibilityState: "hidden", diff --git a/browser/components/newtab/content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx b/browser/components/newtab/content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx index 4efd8c712e..ffcc6b62f4 100644 --- a/browser/components/newtab/content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx +++ b/browser/components/newtab/content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { perfService as perfSvc } from "content-src/lib/perf-service"; import React from "react"; diff --git a/browser/components/newtab/content-src/components/ConfirmDialog/ConfirmDialog.jsx b/browser/components/newtab/content-src/components/ConfirmDialog/ConfirmDialog.jsx index f69e540079..734f261b27 100644 --- a/browser/components/newtab/content-src/components/ConfirmDialog/ConfirmDialog.jsx +++ b/browser/components/newtab/content-src/components/ConfirmDialog/ConfirmDialog.jsx @@ -2,7 +2,7 @@ * 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 { actionCreators as ac, actionTypes } from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes } from "common/Actions.mjs"; import { connect } from "react-redux"; import React from "react"; diff --git a/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx b/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx index 5ea6a57f71..458f65e644 100644 --- a/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx +++ b/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx @@ -26,12 +26,12 @@ export class ContextMenu extends React.PureComponent { componentDidMount() { this.onShow(); setTimeout(() => { - global.addEventListener("click", this.hideContext); + globalThis.addEventListener("click", this.hideContext); }, 0); } componentWillUnmount() { - global.removeEventListener("click", this.hideContext); + globalThis.removeEventListener("click", this.hideContext); } onClick(event) { 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 298dedcee5..1dd13fc965 100644 --- a/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx +++ b/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx @@ -3,8 +3,9 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ import React from "react"; -import { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import { SafeAnchor } from "../../DiscoveryStreamComponents/SafeAnchor/SafeAnchor"; +import { WallpapersSection } from "../../WallpapersSection/WallpapersSection"; export class ContentSection extends React.PureComponent { constructor(props) { @@ -98,6 +99,9 @@ export class ContentSection extends React.PureComponent { mayHaveRecentSaves, openPreferences, spocMessageVariant, + wallpapersEnabled, + activeWallpaper, + setPref, } = this.props; const { topSitesEnabled, @@ -111,6 +115,15 @@ export class ContentSection extends React.PureComponent { return ( <div className="home-section"> + {wallpapersEnabled && ( + <div className="wallpapers-section"> + <h2 data-l10n-id="newtab-wallpaper-title"></h2> + <WallpapersSection + setPref={setPref} + activeWallpaper={activeWallpaper} + /> + </div> + )} <div id="shortcuts-section" className="section"> <moz-toggle id="shortcuts-toggle" diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx b/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx index 54dcd550c4..f1c723fed2 100644 --- a/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx +++ b/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx @@ -2,7 +2,6 @@ * 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 { BackgroundsSection } from "content-src/components/CustomizeMenu/BackgroundsSection/BackgroundsSection"; import { ContentSection } from "content-src/components/CustomizeMenu/ContentSection/ContentSection"; import { connect } from "react-redux"; import React from "react"; @@ -62,11 +61,12 @@ export class _CustomizeMenu extends React.PureComponent { data-l10n-id="newtab-custom-close-button" ref={c => (this.closeButton = c)} /> - <BackgroundsSection /> <ContentSection openPreferences={this.props.openPreferences} setPref={this.props.setPref} enabledSections={this.props.enabledSections} + wallpapersEnabled={this.props.wallpapersEnabled} + activeWallpaper={this.props.activeWallpaper} pocketRegion={this.props.pocketRegion} mayHaveSponsoredTopSites={this.props.mayHaveSponsoredTopSites} mayHaveSponsoredStories={this.props.mayHaveSponsoredStories} diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss b/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss index 579e455a3f..c20da5ce50 100644 --- a/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss +++ b/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss @@ -119,6 +119,10 @@ grid-row-gap: 32px; padding: 0 16px; + .wallpapers-section h2 { + font-size: inherit; + } + .section { moz-toggle { margin-bottom: 10px; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx index 3c31a5a29f..8b9d64dfc1 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { connect } from "react-redux"; import React from "react"; import { SimpleHashRouter } from "./SimpleHashRouter"; @@ -445,9 +442,9 @@ export class CollapseToggle extends React.PureComponent { setBodyClass() { if (this.renderAdmin && !this.state.collapsed) { - global.document.body.classList.add("no-scroll"); + globalThis.document.body.classList.add("no-scroll"); } else { - global.document.body.classList.remove("no-scroll"); + globalThis.document.body.classList.remove("no-scroll"); } } @@ -460,7 +457,7 @@ export class CollapseToggle extends React.PureComponent { } componentWillUnmount() { - global.document.body.classList.remove("no-scroll"); + globalThis.document.body.classList.remove("no-scroll"); } render() { diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/SimpleHashRouter.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/SimpleHashRouter.jsx index 9c3fd8579c..bc7b0c42c5 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/SimpleHashRouter.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/SimpleHashRouter.jsx @@ -8,19 +8,19 @@ export class SimpleHashRouter extends React.PureComponent { constructor(props) { super(props); this.onHashChange = this.onHashChange.bind(this); - this.state = { hash: global.location.hash }; + this.state = { hash: globalThis.location.hash }; } onHashChange() { - this.setState({ hash: global.location.hash }); + this.setState({ hash: globalThis.location.hash }); } componentWillMount() { - global.addEventListener("hashchange", this.onHashChange); + globalThis.addEventListener("hashchange", this.onHashChange); } componentWillUnmount() { - global.removeEventListener("hashchange", this.onHashChange); + globalThis.removeEventListener("hashchange", this.onHashChange); } render() { diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx index 0f0ee51ab9..8b5826dd82 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx @@ -164,7 +164,7 @@ export class _DiscoveryStreamBase extends React.PureComponent { privacyNoticeURL={component.properties.privacyNoticeURL} /> ); - case "CollectionCardGrid": + case "CollectionCardGrid": { const { DiscoveryStream } = this.props; return ( <CollectionCardGrid @@ -178,6 +178,7 @@ export class _DiscoveryStreamBase extends React.PureComponent { dispatch={this.props.dispatch} /> ); + } case "CardGrid": return ( <CardGrid @@ -200,6 +201,7 @@ export class _DiscoveryStreamBase extends React.PureComponent { editorsPicksHeader={component.properties.editorsPicksHeader} recentSavesEnabled={this.props.DiscoveryStream.recentSavesEnabled} hideDescriptions={this.props.DiscoveryStream.hideDescriptions} + firstVisibleTimestamp={this.props.firstVisibleTimestamp} /> ); case "HorizontalRule": @@ -384,6 +386,6 @@ export const DiscoveryStreamBase = connect(state => ({ DiscoveryStream: state.DiscoveryStream, Prefs: state.Prefs, Sections: state.Sections, - document: global.document, + document: globalThis.document, App: state.App, }))(_DiscoveryStreamBase); diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx index cf00361df2..2a9497d1b4 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx @@ -8,10 +8,7 @@ import { DSDismiss } from "content-src/components/DiscoveryStreamComponents/DSDi import { TopicsWidget } from "../TopicsWidget/TopicsWidget.jsx"; import { SafeAnchor } from "../SafeAnchor/SafeAnchor"; import { FluentOrText } from "../../FluentOrText/FluentOrText.jsx"; -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import React, { useEffect, useState, useRef, useCallback } from "react"; import { connect, useSelector } from "react-redux"; const PREF_ONBOARDING_EXPERIENCE_DISMISSED = @@ -31,7 +28,7 @@ export function DSSubHeader({ children }) { ); } -export function OnboardingExperience({ dispatch, windowObj = global }) { +export function OnboardingExperience({ dispatch, windowObj = globalThis }) { const [dismissed, setDismissed] = useState(false); const [maxHeight, setMaxHeight] = useState(null); const heightElement = useRef(null); @@ -361,6 +358,7 @@ export class _CardGrid extends React.PureComponent { url={rec.url} id={rec.id} shim={rec.shim} + fetchTimestamp={rec.fetchTimestamp} type={this.props.type} context={rec.context} sponsor={rec.sponsor} @@ -377,6 +375,7 @@ export class _CardGrid extends React.PureComponent { ctaButtonVariant={ctaButtonVariant} spocMessageVariant={spocMessageVariant} recommendation_id={rec.recommendation_id} + firstVisibleTimestamp={this.props.firstVisibleTimestamp} /> ) ); diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CollectionCardGrid/CollectionCardGrid.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CollectionCardGrid/CollectionCardGrid.jsx index d089a5c8ab..4f3f150a9b 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CollectionCardGrid/CollectionCardGrid.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CollectionCardGrid/CollectionCardGrid.jsx @@ -2,7 +2,7 @@ * 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 { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import { CardGrid } from "content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid"; import { DSDismiss } from "content-src/components/DiscoveryStreamComponents/DSDismiss/DSDismiss"; import { LinkMenuOptions } from "content-src/lib/link-menu-options"; 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 f3e1eab503..b3d965530d 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { DSImage } from "../DSImage/DSImage.jsx"; import { DSLinkMenu } from "../DSLinkMenu/DSLinkMenu"; import { ImpressionStats } from "../../DiscoveryStreamImpressionStats/ImpressionStats"; @@ -198,6 +195,8 @@ export class _DSCard extends React.PureComponent { ...(this.props.shim && this.props.shim.click ? { shim: this.props.shim.click } : {}), + fetchTimestamp: this.props.fetchTimestamp, + firstVisibleTimestamp: this.props.firstVisibleTimestamp, }, }) ); @@ -245,6 +244,8 @@ export class _DSCard extends React.PureComponent { ...(this.props.shim && this.props.shim.save ? { shim: this.props.shim.save } : {}), + fetchTimestamp: this.props.fetchTimestamp, + firstVisibleTimestamp: this.props.firstVisibleTimestamp, }, }) ); @@ -441,10 +442,12 @@ export class _DSCard extends React.PureComponent { ? { shim: this.props.shim.impression } : {}), recommendation_id: this.props.recommendation_id, + fetchTimestamp: this.props.fetchTimestamp, }, ]} dispatch={this.props.dispatch} source={this.props.type} + firstVisibleTimestamp={this.props.firstVisibleTimestamp} /> </SafeAnchor> {ctaButtonVariant === "variant-b" && ( diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx index 6c0641cfc1..80af05c585 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx @@ -2,7 +2,7 @@ * 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 { cardContextTypes } from "../../Card/types.js"; +import { cardContextTypes } from "../../Card/types.mjs"; import { SponsoredContentHighlight } from "../FeatureHighlight/SponsoredContentHighlight"; import { CSSTransition, TransitionGroup } from "react-transition-group"; import { FluentOrText } from "../../FluentOrText/FluentOrText.jsx"; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx index ff3886b407..ed90f68606 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import React from "react"; export class DSEmptyState extends React.PureComponent { diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx index b75063940c..107adca4da 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx @@ -4,7 +4,7 @@ import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu"; import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton"; -import { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import React from "react"; export class DSLinkMenu extends React.PureComponent { diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx index b251fb0401..2275f8b22b 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx @@ -3,10 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ import React from "react"; -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { ModalOverlayWrapper } from "content-src/components/ModalOverlay/ModalOverlay"; export class DSPrivacyModal extends React.PureComponent { diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.jsx index b7e3205646..0a4d687c65 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.jsx @@ -2,7 +2,7 @@ * 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 { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu"; import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton"; import { ImpressionStats } from "../../DiscoveryStreamImpressionStats/ImpressionStats"; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx index 02a3326eb7..fc52decdf8 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx @@ -2,7 +2,7 @@ * 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 { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import { DSDismiss } from "content-src/components/DiscoveryStreamComponents/DSDismiss/DSDismiss"; import { DSImage } from "../DSImage/DSImage.jsx"; import { ImpressionStats } from "../../DiscoveryStreamImpressionStats/ImpressionStats"; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx index 792be40ba3..c650453393 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx @@ -3,7 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ import React, { useState, useCallback, useRef, useEffect } from "react"; -import { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; export function FeatureHighlight({ message, diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx index 1062c3cade..43865c177c 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx @@ -2,7 +2,7 @@ * 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 { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import React from "react"; import { SafeAnchor } from "../SafeAnchor/SafeAnchor"; import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText"; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx index 72ec94e1fe..b586730713 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import React from "react"; export class SafeAnchor extends React.PureComponent { diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx index 1fe2343b94..59b44198a2 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx @@ -3,7 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ import React from "react"; -import { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import { SafeAnchor } from "../SafeAnchor/SafeAnchor"; import { ImpressionStats } from "../../DiscoveryStreamImpressionStats/ImpressionStats"; import { connect } from "react-redux"; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx index 1eb4863271..9342fcd27a 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { TOP_SITES_SOURCE } from "../TopSites/TopSitesConstants"; import React from "react"; @@ -100,7 +97,9 @@ export class ImpressionStats extends React.PureComponent { type: this.props.flightId ? "spoc" : "organic", ...(link.shim ? { shim: link.shim } : {}), recommendation_id: link.recommendation_id, + fetchTimestamp: link.fetchTimestamp, })), + firstVisibleTimestamp: this.props.firstVisibleTimestamp, }) ); this.impressionCardGuids = cards.map(link => link.id); @@ -244,8 +243,8 @@ export class ImpressionStats extends React.PureComponent { } ImpressionStats.defaultProps = { - IntersectionObserver: global.IntersectionObserver, - document: global.document, + IntersectionObserver: globalThis.IntersectionObserver, + document: globalThis.document, rows: [], source: "", }; diff --git a/browser/components/newtab/content-src/components/LinkMenu/LinkMenu.jsx b/browser/components/newtab/content-src/components/LinkMenu/LinkMenu.jsx index 650a03eb95..65b1f38623 100644 --- a/browser/components/newtab/content-src/components/LinkMenu/LinkMenu.jsx +++ b/browser/components/newtab/content-src/components/LinkMenu/LinkMenu.jsx @@ -2,7 +2,7 @@ * 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 { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import { connect } from "react-redux"; import { ContextMenu } from "content-src/components/ContextMenu/ContextMenu"; import { LinkMenuOptions } from "content-src/lib/link-menu-options"; diff --git a/browser/components/newtab/content-src/components/ModalOverlay/ModalOverlay.jsx b/browser/components/newtab/content-src/components/ModalOverlay/ModalOverlay.jsx index fdfdf22db2..5d902b43ba 100644 --- a/browser/components/newtab/content-src/components/ModalOverlay/ModalOverlay.jsx +++ b/browser/components/newtab/content-src/components/ModalOverlay/ModalOverlay.jsx @@ -53,4 +53,4 @@ export class ModalOverlayWrapper extends React.PureComponent { } } -ModalOverlayWrapper.defaultProps = { document: global.document }; +ModalOverlayWrapper.defaultProps = { document: globalThis.document }; diff --git a/browser/components/newtab/content-src/components/Search/Search.jsx b/browser/components/newtab/content-src/components/Search/Search.jsx index 64308963c9..ef7a3757d3 100644 --- a/browser/components/newtab/content-src/components/Search/Search.jsx +++ b/browser/components/newtab/content-src/components/Search/Search.jsx @@ -4,10 +4,7 @@ /* globals ContentSearchUIController, ContentSearchHandoffUIController */ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { connect } from "react-redux"; import { IS_NEWTAB } from "content-src/lib/constants"; import React from "react"; diff --git a/browser/components/newtab/content-src/components/Sections/Sections.jsx b/browser/components/newtab/content-src/components/Sections/Sections.jsx index e72e9145ad..01b50f6918 100644 --- a/browser/components/newtab/content-src/components/Sections/Sections.jsx +++ b/browser/components/newtab/content-src/components/Sections/Sections.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { Card, PlaceholderCard } from "content-src/components/Card/Card"; import { CollapsibleSection } from "content-src/components/CollapsibleSection/CollapsibleSection"; import { ComponentPerfTimer } from "content-src/components/ComponentPerfTimer/ComponentPerfTimer"; @@ -33,7 +30,7 @@ export class Section extends React.PureComponent { let cardsPerRow = CARDS_PER_ROW_DEFAULT; if ( props.compactCards && - global.matchMedia(`(min-width: 1072px)`).matches + globalThis.matchMedia(`(min-width: 1072px)`).matches ) { // If the section has compact cards and the viewport is wide enough, we show // 4 columns instead of 3. @@ -326,7 +323,7 @@ export class Section extends React.PureComponent { } Section.defaultProps = { - document: global.document, + document: globalThis.document, rows: [], emptyState: {}, pref: {}, diff --git a/browser/components/newtab/content-src/components/TopSites/SearchShortcutsForm.jsx b/browser/components/newtab/content-src/components/TopSites/SearchShortcutsForm.jsx index 4324c019f6..2d504c52ab 100644 --- a/browser/components/newtab/content-src/components/TopSites/SearchShortcutsForm.jsx +++ b/browser/components/newtab/content-src/components/TopSites/SearchShortcutsForm.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import React from "react"; import { TOP_SITES_SOURCE } from "./TopSitesConstants"; diff --git a/browser/components/newtab/content-src/components/TopSites/TopSite.jsx b/browser/components/newtab/content-src/components/TopSites/TopSite.jsx index c0932104af..3d63398e0e 100644 --- a/browser/components/newtab/content-src/components/TopSites/TopSite.jsx +++ b/browser/components/newtab/content-src/components/TopSites/TopSite.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { MIN_RICH_FAVICON_SIZE, MIN_SMALL_FAVICON_SIZE, diff --git a/browser/components/newtab/content-src/components/TopSites/TopSiteForm.jsx b/browser/components/newtab/content-src/components/TopSites/TopSiteForm.jsx index 7dd61bdc93..9ca8991735 100644 --- a/browser/components/newtab/content-src/components/TopSites/TopSiteForm.jsx +++ b/browser/components/newtab/content-src/components/TopSites/TopSiteForm.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { A11yLinkButton } from "content-src/components/A11yLinkButton/A11yLinkButton"; import React from "react"; import { TOP_SITES_SOURCE } from "./TopSitesConstants"; diff --git a/browser/components/newtab/content-src/components/TopSites/TopSiteImpressionWrapper.jsx b/browser/components/newtab/content-src/components/TopSites/TopSiteImpressionWrapper.jsx index 580809dd57..b654a803c7 100644 --- a/browser/components/newtab/content-src/components/TopSites/TopSiteImpressionWrapper.jsx +++ b/browser/components/newtab/content-src/components/TopSites/TopSiteImpressionWrapper.jsx @@ -2,7 +2,7 @@ * 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 { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import React from "react"; const VISIBLE = "visible"; @@ -142,8 +142,8 @@ export class TopSiteImpressionWrapper extends React.PureComponent { } TopSiteImpressionWrapper.defaultProps = { - IntersectionObserver: global.IntersectionObserver, - document: global.document, + IntersectionObserver: globalThis.IntersectionObserver, + document: globalThis.document, actionType: null, tile: null, }; diff --git a/browser/components/newtab/content-src/components/TopSites/TopSites.jsx b/browser/components/newtab/content-src/components/TopSites/TopSites.jsx index ba7676fd10..d9a12aa97d 100644 --- a/browser/components/newtab/content-src/components/TopSites/TopSites.jsx +++ b/browser/components/newtab/content-src/components/TopSites/TopSites.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { MIN_RICH_FAVICON_SIZE, TOP_SITES_SOURCE } from "./TopSitesConstants"; import { CollapsibleSection } from "content-src/components/CollapsibleSection/CollapsibleSection"; import { ComponentPerfTimer } from "content-src/components/ComponentPerfTimer/ComponentPerfTimer"; @@ -93,7 +90,7 @@ export class _TopSites extends React.PureComponent { // We hide 2 sites per row when not in the wide layout. let sitesPerRow = TOP_SITES_MAX_SITES_PER_ROW; // $break-point-widest = 1072px (from _variables.scss) - if (!global.matchMedia(`(min-width: 1072px)`).matches) { + if (!globalThis.matchMedia(`(min-width: 1072px)`).matches) { sitesPerRow -= 2; } return this.props.TopSites.rows.slice( diff --git a/browser/components/newtab/content-src/components/TopSites/TopSitesConstants.js b/browser/components/newtab/content-src/components/TopSites/TopSitesConstants.mjs index f488896238..f488896238 100644 --- a/browser/components/newtab/content-src/components/TopSites/TopSitesConstants.js +++ b/browser/components/newtab/content-src/components/TopSites/TopSitesConstants.mjs diff --git a/browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx b/browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx new file mode 100644 index 0000000000..0b51a146f5 --- /dev/null +++ b/browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx @@ -0,0 +1,100 @@ +/* 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 React from "react"; +import { connect } from "react-redux"; + +export class _WallpapersSection extends React.PureComponent { + constructor(props) { + super(props); + this.handleChange = this.handleChange.bind(this); + this.handleReset = this.handleReset.bind(this); + this.prefersHighContrastQuery = null; + this.prefersDarkQuery = null; + } + + componentDidMount() { + this.prefersDarkQuery = globalThis.matchMedia( + "(prefers-color-scheme: dark)" + ); + } + + handleChange(event) { + const { id } = event.target; + const prefs = this.props.Prefs.values; + const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; + this.props.setPref(`newtabWallpapers.wallpaper-${colorMode}`, id); + // bug 1892095 + if ( + prefs["newtabWallpapers.wallpaper-dark"] === "" && + colorMode === "light" + ) { + this.props.setPref( + "newtabWallpapers.wallpaper-dark", + id.replace("light", "dark") + ); + } + + if ( + prefs["newtabWallpapers.wallpaper-light"] === "" && + colorMode === "dark" + ) { + this.props.setPref( + `newtabWallpapers.wallpaper-light`, + id.replace("dark", "light") + ); + } + } + + handleReset() { + const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; + this.props.setPref(`newtabWallpapers.wallpaper-${colorMode}`, ""); + } + + render() { + const { wallpaperList } = this.props.Wallpapers; + const { activeWallpaper } = this.props; + return ( + <div> + <fieldset className="wallpaper-list"> + {wallpaperList.map(({ title, theme, fluent_id }) => { + return ( + <> + <input + onChange={this.handleChange} + type="radio" + name={`wallpaper-${title}`} + id={title} + value={title} + checked={title === activeWallpaper} + aria-checked={title === activeWallpaper} + className={`wallpaper-input theme-${theme} ${title}`} + /> + <label + htmlFor={title} + className="sr-only" + data-l10n-id={fluent_id} + > + {fluent_id} + </label> + </> + ); + })} + </fieldset> + <button + className="wallpapers-reset" + onClick={this.handleReset} + data-l10n-id="newtab-wallpaper-reset" + /> + </div> + ); + } +} + +export const WallpapersSection = connect(state => { + return { + Wallpapers: state.Wallpapers, + Prefs: state.Prefs, + }; +})(_WallpapersSection); diff --git a/browser/components/newtab/content-src/components/WallpapersSection/_WallpapersSection.scss b/browser/components/newtab/content-src/components/WallpapersSection/_WallpapersSection.scss new file mode 100644 index 0000000000..689661750b --- /dev/null +++ b/browser/components/newtab/content-src/components/WallpapersSection/_WallpapersSection.scss @@ -0,0 +1,87 @@ +.wallpaper-list { + display: grid; + gap: 16px; + grid-template-columns: 1fr 1fr 1fr; + grid-auto-rows: 86px; + margin: 16px 0; + padding: 0; + border: none; + + .wallpaper-input, + .sr-only { + &.theme-light { + display: inline-block; + + @include dark-theme-only { + display: none; + } + } + + &.theme-dark { + display: none; + + @include dark-theme-only { + display: inline-block; + } + } + } + + .wallpaper-input { + appearance: none; + margin: 0; + padding: 0; + height: 86px; + width: 100%; + box-shadow: $shadow-secondary; + border-radius: 8px; + background-clip: content-box; + background-repeat: no-repeat; + background-size: cover; + cursor: pointer; + outline: 2px solid transparent; + + $wallpapers: dark-landscape, dark-color, dark-mountain, dark-panda, dark-sky, dark-beach, light-beach, light-color, light-landscape, light-mountain, light-panda, light-sky; + + @each $wallpaper in $wallpapers { + &.#{$wallpaper} { + background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/#{$wallpaper}.avif') + } + } + + &:checked { + outline-color: var(--color-accent-primary-active); + } + + &:focus-visible { + outline-color: var(--newtab-primary-action-background); + } + + &:hover { + filter: brightness(55%); + outline-color: transparent; + } + } + + // visually hide label, but still read by screen readers + .sr-only { + opacity: 0; + overflow: hidden; + position: absolute; + pointer-events: none; + } +} + +.wallpapers-reset { + background: none; + border: none; + text-decoration: underline; + margin-inline: auto; + display: block; + font-size: var(--font-size-small); + color: var(--newtab-text-primary-color); + cursor: pointer; + + &:hover { + text-decoration: none; + } +} diff --git a/browser/components/newtab/content-src/lib/constants.js b/browser/components/newtab/content-src/lib/constants.mjs index 2c96160b4b..4f07a77e29 100644 --- a/browser/components/newtab/content-src/lib/constants.js +++ b/browser/components/newtab/content-src/lib/constants.mjs @@ -3,7 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ export const IS_NEWTAB = - global.document && global.document.documentURI === "about:newtab"; + globalThis.document && globalThis.document.documentURI === "about:newtab"; export const NEWTAB_DARK_THEME = { ntp_background: { r: 42, diff --git a/browser/components/newtab/content-src/lib/detect-user-session-start.js b/browser/components/newtab/content-src/lib/detect-user-session-start.mjs index 43aa388967..d4c36efd4a 100644 --- a/browser/components/newtab/content-src/lib/detect-user-session-start.js +++ b/browser/components/newtab/content-src/lib/detect-user-session-start.mjs @@ -5,8 +5,8 @@ import { actionCreators as ac, actionTypes as at, -} from "common/Actions.sys.mjs"; -import { perfService as perfSvc } from "content-src/lib/perf-service"; +} from "../../common/Actions.mjs"; +import { perfService as perfSvc } from "./perf-service.mjs"; const VISIBLE = "visible"; const VISIBILITY_CHANGE_EVENT = "visibilitychange"; @@ -15,7 +15,7 @@ export class DetectUserSessionStart { constructor(store, options = {}) { this._store = store; // Overrides for testing - this.document = options.document || global.document; + this.document = options.document || globalThis.document; this._perfService = options.perfService || perfSvc; this._onVisibilityChange = this._onVisibilityChange.bind(this); } diff --git a/browser/components/newtab/content-src/lib/init-store.js b/browser/components/newtab/content-src/lib/init-store.mjs index f0ab2db86a..85b3b0b470 100644 --- a/browser/components/newtab/content-src/lib/init-store.js +++ b/browser/components/newtab/content-src/lib/init-store.mjs @@ -8,7 +8,10 @@ import { actionCreators as ac, actionTypes as at, actionUtils as au, -} from "common/Actions.sys.mjs"; +} from "../../common/Actions.mjs"; +// We disable import checking here as redux is installed via the npm packages +// at the newtab level, rather than in the top-level package.json. +// eslint-disable-next-line import/no-unresolved import { applyMiddleware, combineReducers, createStore } from "redux"; export const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE"; @@ -117,12 +120,12 @@ export function initStore(reducers, initialState) { const store = createStore( mergeStateReducer(combineReducers(reducers)), initialState, - global.RPMAddMessageListener && + globalThis.RPMAddMessageListener && applyMiddleware(rehydrationMiddleware, messageMiddleware) ); - if (global.RPMAddMessageListener) { - global.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => { + if (globalThis.RPMAddMessageListener) { + globalThis.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => { try { store.dispatch(msg.data); } catch (ex) { diff --git a/browser/components/newtab/content-src/lib/link-menu-options.js b/browser/components/newtab/content-src/lib/link-menu-options.mjs index 12e47259c1..f10a5e34c6 100644 --- a/browser/components/newtab/content-src/lib/link-menu-options.js +++ b/browser/components/newtab/content-src/lib/link-menu-options.mjs @@ -5,7 +5,7 @@ import { actionCreators as ac, actionTypes as at, -} from "common/Actions.sys.mjs"; +} from "../../common/Actions.mjs"; const _OpenInPrivateWindow = site => ({ id: "newtab-menu-open-new-private-window", diff --git a/browser/components/newtab/content-src/lib/perf-service.js b/browser/components/newtab/content-src/lib/perf-service.mjs index 6ea99ce877..25fc430726 100644 --- a/browser/components/newtab/content-src/lib/perf-service.js +++ b/browser/components/newtab/content-src/lib/perf-service.mjs @@ -2,8 +2,6 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - let usablePerfObj = window.performance; export function _PerfService(options) { @@ -37,8 +35,8 @@ _PerfService.prototype = { * @param {String} type eg "mark" * @return {Array} Performance* objects */ - getEntriesByName: function getEntriesByName(name, type) { - return this._perf.getEntriesByName(name, type); + getEntriesByName: function getEntriesByName(entryName, type) { + return this._perf.getEntriesByName(entryName, type); }, /** @@ -89,11 +87,11 @@ _PerfService.prototype = { * See [bug 1369303](https://bugzilla.mozilla.org/show_bug.cgi?id=1369303) * for more info. */ - getMostRecentAbsMarkStartByName(name) { - let entries = this.getEntriesByName(name, "mark"); + getMostRecentAbsMarkStartByName(entryName) { + let entries = this.getEntriesByName(entryName, "mark"); if (!entries.length) { - throw new Error(`No marks with the name ${name}`); + throw new Error(`No marks with the name ${entryName}`); } let mostRecentEntry = entries[entries.length - 1]; diff --git a/browser/components/newtab/content-src/lib/screenshot-utils.js b/browser/components/newtab/content-src/lib/screenshot-utils.mjs index 7ea93f12ae..2d1342be4f 100644 --- a/browser/components/newtab/content-src/lib/screenshot-utils.js +++ b/browser/components/newtab/content-src/lib/screenshot-utils.mjs @@ -30,7 +30,7 @@ export const ScreenshotUtils = { } if (this.isBlob(false, remoteImage)) { return { - url: global.URL.createObjectURL(remoteImage.data), + url: globalThis.URL.createObjectURL(remoteImage.data), path: remoteImage.path, }; } @@ -41,7 +41,7 @@ export const ScreenshotUtils = { // This should always be called with a local image and not a remote image. maybeRevokeBlobObjectURL(localImage) { if (this.isBlob(true, localImage)) { - global.URL.revokeObjectURL(localImage.url); + globalThis.URL.revokeObjectURL(localImage.url); } }, diff --git a/browser/components/newtab/content-src/lib/selectLayoutRender.js b/browser/components/newtab/content-src/lib/selectLayoutRender.mjs index 8ef4dd428f..8ef4dd428f 100644 --- a/browser/components/newtab/content-src/lib/selectLayoutRender.js +++ b/browser/components/newtab/content-src/lib/selectLayoutRender.mjs diff --git a/browser/components/newtab/content-src/styles/_activity-stream.scss b/browser/components/newtab/content-src/styles/_activity-stream.scss index 88ed530b6a..d2e66667b2 100644 --- a/browser/components/newtab/content-src/styles/_activity-stream.scss +++ b/browser/components/newtab/content-src/styles/_activity-stream.scss @@ -21,6 +21,17 @@ body { background-color: var(--newtab-background-color); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif; font-size: 16px; + + // rules for HNT wallpapers + background-repeat: no-repeat; + background-size: cover; + background-position: center; + background-attachment: fixed; + background-image: var(--newtab-wallpaper-light, ''); + + @media (prefers-color-scheme: dark) { + background-image: var(--newtab-wallpaper-dark, ''); + } } .no-scroll { @@ -137,6 +148,7 @@ input { @import '../components/ContextMenu/ContextMenu'; @import '../components/ConfirmDialog/ConfirmDialog'; @import '../components/CustomizeMenu/CustomizeMenu'; +@import '../components/WallpapersSection/WallpapersSection'; @import '../components/Card/Card'; @import '../components/CollapsibleSection/CollapsibleSection'; @import '../components/DiscoveryStreamAdmin/DiscoveryStreamAdmin'; diff --git a/browser/components/newtab/css/activity-stream-linux.css b/browser/components/newtab/css/activity-stream-linux.css index 8773159737..131ffac535 100644 --- a/browser/components/newtab/css/activity-stream-linux.css +++ b/browser/components/newtab/css/activity-stream-linux.css @@ -276,6 +276,16 @@ body { background-color: var(--newtab-background-color); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Ubuntu, "Helvetica Neue", sans-serif; font-size: 16px; + background-repeat: no-repeat; + background-size: cover; + background-position: center; + background-attachment: fixed; + background-image: var(--newtab-wallpaper-light, ""); +} +@media (prefers-color-scheme: dark) { + body { + background-image: var(--newtab-wallpaper-dark, ""); + } } .no-scroll { @@ -405,10 +415,16 @@ input[type=text], input[type=search] { } main { - margin: auto; + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; width: 274px; padding: 0; } +main .vertical-center-wrapper { + margin: auto 0; +} main section { margin-bottom: 20px; position: relative; @@ -489,6 +505,29 @@ main section { background-color: var(--newtab-element-active-color); } +.wallpaper-attribution { + padding: 0 25px; + font-size: 14px; +} +.wallpaper-attribution.theme-light { + display: inline-block; +} +[lwt-newtab-brighttext] .wallpaper-attribution.theme-light { + display: none; +} +.wallpaper-attribution.theme-dark { + display: none; +} +[lwt-newtab-brighttext] .wallpaper-attribution.theme-dark { + display: inline-block; +} +.wallpaper-attribution a { + color: var(--newtab-element-color); +} +.wallpaper-attribution a:hover { + text-decoration: none; +} + .as-error-fallback { align-items: center; border-radius: 3px; @@ -1694,6 +1733,9 @@ main section { grid-row-gap: 32px; padding: 0 16px; } +.home-section .wallpapers-section h2 { + font-size: inherit; +} .home-section .section moz-toggle { margin-bottom: 10px; } @@ -1830,6 +1872,112 @@ main section { box-shadow: 0 0 0 2px var(--newtab-primary-action-background-dimmed); } +.wallpaper-list { + display: grid; + gap: 16px; + grid-template-columns: 1fr 1fr 1fr; + grid-auto-rows: 86px; + margin: 16px 0; + padding: 0; + border: none; +} +.wallpaper-list .wallpaper-input.theme-light, +.wallpaper-list .sr-only.theme-light { + display: inline-block; +} +[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-light, +[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-light { + display: none; +} +.wallpaper-list .wallpaper-input.theme-dark, +.wallpaper-list .sr-only.theme-dark { + display: none; +} +[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-dark, +[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-dark { + display: inline-block; +} +.wallpaper-list .wallpaper-input { + appearance: none; + margin: 0; + padding: 0; + height: 86px; + width: 100%; + box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2); + border-radius: 8px; + background-clip: content-box; + background-repeat: no-repeat; + background-size: cover; + cursor: pointer; + outline: 2px solid transparent; +} +.wallpaper-list .wallpaper-input.dark-landscape { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-landscape.avif"); +} +.wallpaper-list .wallpaper-input.dark-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-color.avif"); +} +.wallpaper-list .wallpaper-input.dark-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-mountain.avif"); +} +.wallpaper-list .wallpaper-input.dark-panda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-panda.avif"); +} +.wallpaper-list .wallpaper-input.dark-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-sky.avif"); +} +.wallpaper-list .wallpaper-input.dark-beach { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-beach.avif"); +} +.wallpaper-list .wallpaper-input.light-beach { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-beach.avif"); +} +.wallpaper-list .wallpaper-input.light-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-color.avif"); +} +.wallpaper-list .wallpaper-input.light-landscape { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-landscape.avif"); +} +.wallpaper-list .wallpaper-input.light-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-mountain.avif"); +} +.wallpaper-list .wallpaper-input.light-panda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-panda.avif"); +} +.wallpaper-list .wallpaper-input.light-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-sky.avif"); +} +.wallpaper-list .wallpaper-input:checked { + outline-color: var(--color-accent-primary-active); +} +.wallpaper-list .wallpaper-input:focus-visible { + outline-color: var(--newtab-primary-action-background); +} +.wallpaper-list .wallpaper-input:hover { + filter: brightness(55%); + outline-color: transparent; +} +.wallpaper-list .sr-only { + opacity: 0; + overflow: hidden; + position: absolute; + pointer-events: none; +} + +.wallpapers-reset { + background: none; + border: none; + text-decoration: underline; + margin-inline: auto; + display: block; + font-size: var(--font-size-small); + color: var(--newtab-text-primary-color); + cursor: pointer; +} +.wallpapers-reset:hover { + text-decoration: none; +} + /* stylelint-disable max-nesting-depth */ .card-outer { background: var(--newtab-background-color-secondary); diff --git a/browser/components/newtab/css/activity-stream-mac.css b/browser/components/newtab/css/activity-stream-mac.css index 87b942818a..416209d511 100644 --- a/browser/components/newtab/css/activity-stream-mac.css +++ b/browser/components/newtab/css/activity-stream-mac.css @@ -280,6 +280,16 @@ body { background-color: var(--newtab-background-color); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Ubuntu, "Helvetica Neue", sans-serif; font-size: 16px; + background-repeat: no-repeat; + background-size: cover; + background-position: center; + background-attachment: fixed; + background-image: var(--newtab-wallpaper-light, ""); +} +@media (prefers-color-scheme: dark) { + body { + background-image: var(--newtab-wallpaper-dark, ""); + } } .no-scroll { @@ -409,10 +419,16 @@ input[type=text], input[type=search] { } main { - margin: auto; + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; width: 274px; padding: 0; } +main .vertical-center-wrapper { + margin: auto 0; +} main section { margin-bottom: 20px; position: relative; @@ -493,6 +509,29 @@ main section { background-color: var(--newtab-element-active-color); } +.wallpaper-attribution { + padding: 0 25px; + font-size: 14px; +} +.wallpaper-attribution.theme-light { + display: inline-block; +} +[lwt-newtab-brighttext] .wallpaper-attribution.theme-light { + display: none; +} +.wallpaper-attribution.theme-dark { + display: none; +} +[lwt-newtab-brighttext] .wallpaper-attribution.theme-dark { + display: inline-block; +} +.wallpaper-attribution a { + color: var(--newtab-element-color); +} +.wallpaper-attribution a:hover { + text-decoration: none; +} + .as-error-fallback { align-items: center; border-radius: 3px; @@ -1698,6 +1737,9 @@ main section { grid-row-gap: 32px; padding: 0 16px; } +.home-section .wallpapers-section h2 { + font-size: inherit; +} .home-section .section moz-toggle { margin-bottom: 10px; } @@ -1834,6 +1876,112 @@ main section { box-shadow: 0 0 0 2px var(--newtab-primary-action-background-dimmed); } +.wallpaper-list { + display: grid; + gap: 16px; + grid-template-columns: 1fr 1fr 1fr; + grid-auto-rows: 86px; + margin: 16px 0; + padding: 0; + border: none; +} +.wallpaper-list .wallpaper-input.theme-light, +.wallpaper-list .sr-only.theme-light { + display: inline-block; +} +[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-light, +[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-light { + display: none; +} +.wallpaper-list .wallpaper-input.theme-dark, +.wallpaper-list .sr-only.theme-dark { + display: none; +} +[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-dark, +[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-dark { + display: inline-block; +} +.wallpaper-list .wallpaper-input { + appearance: none; + margin: 0; + padding: 0; + height: 86px; + width: 100%; + box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2); + border-radius: 8px; + background-clip: content-box; + background-repeat: no-repeat; + background-size: cover; + cursor: pointer; + outline: 2px solid transparent; +} +.wallpaper-list .wallpaper-input.dark-landscape { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-landscape.avif"); +} +.wallpaper-list .wallpaper-input.dark-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-color.avif"); +} +.wallpaper-list .wallpaper-input.dark-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-mountain.avif"); +} +.wallpaper-list .wallpaper-input.dark-panda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-panda.avif"); +} +.wallpaper-list .wallpaper-input.dark-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-sky.avif"); +} +.wallpaper-list .wallpaper-input.dark-beach { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-beach.avif"); +} +.wallpaper-list .wallpaper-input.light-beach { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-beach.avif"); +} +.wallpaper-list .wallpaper-input.light-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-color.avif"); +} +.wallpaper-list .wallpaper-input.light-landscape { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-landscape.avif"); +} +.wallpaper-list .wallpaper-input.light-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-mountain.avif"); +} +.wallpaper-list .wallpaper-input.light-panda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-panda.avif"); +} +.wallpaper-list .wallpaper-input.light-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-sky.avif"); +} +.wallpaper-list .wallpaper-input:checked { + outline-color: var(--color-accent-primary-active); +} +.wallpaper-list .wallpaper-input:focus-visible { + outline-color: var(--newtab-primary-action-background); +} +.wallpaper-list .wallpaper-input:hover { + filter: brightness(55%); + outline-color: transparent; +} +.wallpaper-list .sr-only { + opacity: 0; + overflow: hidden; + position: absolute; + pointer-events: none; +} + +.wallpapers-reset { + background: none; + border: none; + text-decoration: underline; + margin-inline: auto; + display: block; + font-size: var(--font-size-small); + color: var(--newtab-text-primary-color); + cursor: pointer; +} +.wallpapers-reset:hover { + text-decoration: none; +} + /* stylelint-disable max-nesting-depth */ .card-outer { background: var(--newtab-background-color-secondary); diff --git a/browser/components/newtab/css/activity-stream-windows.css b/browser/components/newtab/css/activity-stream-windows.css index 25370fdf19..f6118e3c18 100644 --- a/browser/components/newtab/css/activity-stream-windows.css +++ b/browser/components/newtab/css/activity-stream-windows.css @@ -276,6 +276,16 @@ body { background-color: var(--newtab-background-color); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Ubuntu, "Helvetica Neue", sans-serif; font-size: 16px; + background-repeat: no-repeat; + background-size: cover; + background-position: center; + background-attachment: fixed; + background-image: var(--newtab-wallpaper-light, ""); +} +@media (prefers-color-scheme: dark) { + body { + background-image: var(--newtab-wallpaper-dark, ""); + } } .no-scroll { @@ -405,10 +415,16 @@ input[type=text], input[type=search] { } main { - margin: auto; + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; width: 274px; padding: 0; } +main .vertical-center-wrapper { + margin: auto 0; +} main section { margin-bottom: 20px; position: relative; @@ -489,6 +505,29 @@ main section { background-color: var(--newtab-element-active-color); } +.wallpaper-attribution { + padding: 0 25px; + font-size: 14px; +} +.wallpaper-attribution.theme-light { + display: inline-block; +} +[lwt-newtab-brighttext] .wallpaper-attribution.theme-light { + display: none; +} +.wallpaper-attribution.theme-dark { + display: none; +} +[lwt-newtab-brighttext] .wallpaper-attribution.theme-dark { + display: inline-block; +} +.wallpaper-attribution a { + color: var(--newtab-element-color); +} +.wallpaper-attribution a:hover { + text-decoration: none; +} + .as-error-fallback { align-items: center; border-radius: 3px; @@ -1694,6 +1733,9 @@ main section { grid-row-gap: 32px; padding: 0 16px; } +.home-section .wallpapers-section h2 { + font-size: inherit; +} .home-section .section moz-toggle { margin-bottom: 10px; } @@ -1830,6 +1872,112 @@ main section { box-shadow: 0 0 0 2px var(--newtab-primary-action-background-dimmed); } +.wallpaper-list { + display: grid; + gap: 16px; + grid-template-columns: 1fr 1fr 1fr; + grid-auto-rows: 86px; + margin: 16px 0; + padding: 0; + border: none; +} +.wallpaper-list .wallpaper-input.theme-light, +.wallpaper-list .sr-only.theme-light { + display: inline-block; +} +[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-light, +[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-light { + display: none; +} +.wallpaper-list .wallpaper-input.theme-dark, +.wallpaper-list .sr-only.theme-dark { + display: none; +} +[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-dark, +[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-dark { + display: inline-block; +} +.wallpaper-list .wallpaper-input { + appearance: none; + margin: 0; + padding: 0; + height: 86px; + width: 100%; + box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2); + border-radius: 8px; + background-clip: content-box; + background-repeat: no-repeat; + background-size: cover; + cursor: pointer; + outline: 2px solid transparent; +} +.wallpaper-list .wallpaper-input.dark-landscape { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-landscape.avif"); +} +.wallpaper-list .wallpaper-input.dark-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-color.avif"); +} +.wallpaper-list .wallpaper-input.dark-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-mountain.avif"); +} +.wallpaper-list .wallpaper-input.dark-panda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-panda.avif"); +} +.wallpaper-list .wallpaper-input.dark-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-sky.avif"); +} +.wallpaper-list .wallpaper-input.dark-beach { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-beach.avif"); +} +.wallpaper-list .wallpaper-input.light-beach { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-beach.avif"); +} +.wallpaper-list .wallpaper-input.light-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-color.avif"); +} +.wallpaper-list .wallpaper-input.light-landscape { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-landscape.avif"); +} +.wallpaper-list .wallpaper-input.light-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-mountain.avif"); +} +.wallpaper-list .wallpaper-input.light-panda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-panda.avif"); +} +.wallpaper-list .wallpaper-input.light-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-sky.avif"); +} +.wallpaper-list .wallpaper-input:checked { + outline-color: var(--color-accent-primary-active); +} +.wallpaper-list .wallpaper-input:focus-visible { + outline-color: var(--newtab-primary-action-background); +} +.wallpaper-list .wallpaper-input:hover { + filter: brightness(55%); + outline-color: transparent; +} +.wallpaper-list .sr-only { + opacity: 0; + overflow: hidden; + position: absolute; + pointer-events: none; +} + +.wallpapers-reset { + background: none; + border: none; + text-decoration: underline; + margin-inline: auto; + display: block; + font-size: var(--font-size-small); + color: var(--newtab-text-primary-color); + cursor: pointer; +} +.wallpapers-reset:hover { + text-decoration: none; +} + /* stylelint-disable max-nesting-depth */ .card-outer { background: var(--newtab-background-color-secondary); diff --git a/browser/components/newtab/data/content/activity-stream.bundle.js b/browser/components/newtab/data/content/activity-stream.bundle.js index 8904ba87d1..395e8c5bb3 100644 --- a/browser/components/newtab/data/content/activity-stream.bundle.js +++ b/browser/components/newtab/data/content/activity-stream.bundle.js @@ -70,11 +70,13 @@ __webpack_require__.d(__webpack_exports__, { renderWithoutState: () => (/* binding */ renderWithoutState) }); -;// CONCATENATED MODULE: ./common/Actions.sys.mjs +;// CONCATENATED MODULE: ./common/Actions.mjs /* 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/. */ +// This file is accessed from both content and system scopes. + const MAIN_MESSAGE_TYPE = "ActivityStream:Main"; const CONTENT_MESSAGE_TYPE = "ActivityStream:Content"; const PRELOAD_MESSAGE_TYPE = "ActivityStream:PreloadedBrowser"; @@ -231,6 +233,7 @@ for (const type of [ "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", + "WALLPAPERS_SET", "WEBEXT_CLICK", "WEBEXT_DISMISS", ]) { @@ -444,8 +447,11 @@ function DiscoveryStreamLoadedContent( return importContext === UI_CODE ? AlsoToMain(action) : action; } -function SetPref(name, value, importContext = globalImportContext) { - const action = { type: actionTypes.SET_PREF, data: { name, value } }; +function SetPref(prefName, value, importContext = globalImportContext) { + const action = { + type: actionTypes.SET_PREF, + data: { name: prefName, value }, + }; return importContext === UI_CODE ? AlsoToMain(action) : action; } @@ -545,19 +551,19 @@ class SimpleHashRouter extends (external_React_default()).PureComponent { super(props); this.onHashChange = this.onHashChange.bind(this); this.state = { - hash: __webpack_require__.g.location.hash + hash: globalThis.location.hash }; } onHashChange() { this.setState({ - hash: __webpack_require__.g.location.hash + hash: globalThis.location.hash }); } componentWillMount() { - __webpack_require__.g.addEventListener("hashchange", this.onHashChange); + globalThis.addEventListener("hashchange", this.onHashChange); } componentWillUnmount() { - __webpack_require__.g.removeEventListener("hashchange", this.onHashChange); + globalThis.removeEventListener("hashchange", this.onHashChange); } render() { const [, ...routes] = this.state.hash.split("-"); @@ -882,9 +888,9 @@ class CollapseToggle extends (external_React_default()).PureComponent { } setBodyClass() { if (this.renderAdmin && !this.state.collapsed) { - __webpack_require__.g.document.body.classList.add("no-scroll"); + globalThis.document.body.classList.add("no-scroll"); } else { - __webpack_require__.g.document.body.classList.remove("no-scroll"); + globalThis.document.body.classList.remove("no-scroll"); } } componentDidMount() { @@ -894,7 +900,7 @@ class CollapseToggle extends (external_React_default()).PureComponent { this.setBodyClass(); } componentWillUnmount() { - __webpack_require__.g.document.body.classList.remove("no-scroll"); + globalThis.document.body.classList.remove("no-scroll"); } render() { const { @@ -1262,11 +1268,11 @@ class ContextMenu extends (external_React_default()).PureComponent { componentDidMount() { this.onShow(); setTimeout(() => { - __webpack_require__.g.addEventListener("click", this.hideContext); + globalThis.addEventListener("click", this.hideContext); }, 0); } componentWillUnmount() { - __webpack_require__.g.removeEventListener("click", this.hideContext); + globalThis.removeEventListener("click", this.hideContext); } onClick(event) { // Eat all clicks on the context menu so they don't bubble up to window. @@ -1392,23 +1398,21 @@ class _ContextMenuItem extends (external_React_default()).PureComponent { const ContextMenuItem = (0,external_ReactRedux_namespaceObject.connect)(state => ({ Prefs: state.Prefs }))(_ContextMenuItem); -;// CONCATENATED MODULE: ./content-src/lib/link-menu-options.js +;// CONCATENATED MODULE: ./content-src/lib/link-menu-options.mjs /* 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 _OpenInPrivateWindow = site => ({ id: "newtab-menu-open-new-private-window", icon: "new-window-private", action: actionCreators.OnlyToMain({ type: actionTypes.OPEN_PRIVATE_WINDOW, - data: { - url: site.url, - referrer: site.referrer - } + data: { url: site.url, referrer: site.referrer }, }), - userEvent: "OPEN_PRIVATE_WINDOW" + userEvent: "OPEN_PRIVATE_WINDOW", }); /** @@ -1417,19 +1421,15 @@ const _OpenInPrivateWindow = site => ({ * the index of the site. */ const LinkMenuOptions = { - Separator: () => ({ - type: "separator" - }), - EmptyItem: () => ({ - type: "empty" - }), + Separator: () => ({ type: "separator" }), + EmptyItem: () => ({ type: "empty" }), ShowPrivacyInfo: () => ({ id: "newtab-menu-show-privacy-info", icon: "info", action: { - type: actionTypes.SHOW_PRIVACY_INFO + type: actionTypes.SHOW_PRIVACY_INFO, }, - userEvent: "SHOW_PRIVACY_INFO" + userEvent: "SHOW_PRIVACY_INFO", }), AboutSponsored: site => ({ id: "newtab-menu-show-privacy-info", @@ -1439,32 +1439,28 @@ const LinkMenuOptions = { data: { advertiser_name: (site.label || site.hostname).toLocaleLowerCase(), position: site.sponsored_position, - tile_id: site.sponsored_tile_id - } + tile_id: site.sponsored_tile_id, + }, }), - userEvent: "TOPSITE_SPONSOR_INFO" + userEvent: "TOPSITE_SPONSOR_INFO", }), RemoveBookmark: site => ({ id: "newtab-menu-remove-bookmark", icon: "bookmark-added", action: actionCreators.AlsoToMain({ type: actionTypes.DELETE_BOOKMARK_BY_ID, - data: site.bookmarkGuid + data: site.bookmarkGuid, }), - userEvent: "BOOKMARK_DELETE" + userEvent: "BOOKMARK_DELETE", }), AddBookmark: site => ({ id: "newtab-menu-bookmark", icon: "bookmark-hollow", action: actionCreators.AlsoToMain({ type: actionTypes.BOOKMARK_URL, - data: { - url: site.url, - title: site.title, - type: site.type - } + data: { url: site.url, title: site.title, type: site.type }, }), - userEvent: "BOOKMARK_ADD" + userEvent: "BOOKMARK_ADD", }), OpenInNewWindow: site => ({ id: "newtab-menu-open-new-window", @@ -1475,10 +1471,10 @@ const LinkMenuOptions = { referrer: site.referrer, typedBonus: site.typedBonus, url: site.url, - sponsored_tile_id: site.sponsored_tile_id - } + sponsored_tile_id: site.sponsored_tile_id, + }, }), - userEvent: "OPEN_NEW_WINDOW" + userEvent: "OPEN_NEW_WINDOW", }), // This blocks the url for regular stories, // but also sends a message to DiscoveryStream with flight_id. @@ -1499,20 +1495,20 @@ const LinkMenuOptions = { pocket_id: site.pocket_id, // used by PlacesFeed and TopSitesFeed for sponsored top sites blocking. isSponsoredTopSite: site.sponsored_position, - ...(site.flight_id ? { - flight_id: site.flight_id - } : {}), + ...(site.flight_id ? { flight_id: site.flight_id } : {}), // If not sponsored, hostname could be anything (Cat3 Data!). // So only put in advertiser_name for sponsored topsites. - ...(site.sponsored_position ? { - advertiser_name: (site.label || site.hostname)?.toLocaleLowerCase() - } : {}), + ...(site.sponsored_position + ? { + advertiser_name: ( + site.label || site.hostname + )?.toLocaleLowerCase(), + } + : {}), position: pos, - ...(site.sponsored_tile_id ? { - tile_id: site.sponsored_tile_id - } : {}), - is_pocket_card: site.type === "CardGrid" - })) + ...(site.sponsored_tile_id ? { tile_id: site.sponsored_tile_id } : {}), + is_pocket_card: site.type === "CardGrid", + })), }), impression: actionCreators.ImpressionStats({ source: eventSource, @@ -1520,13 +1516,12 @@ const LinkMenuOptions = { tiles: tiles.map((site, index) => ({ id: site.guid, pos: pos + index, - ...(site.shim && site.shim.delete ? { - shim: site.shim.delete - } : {}) - })) + ...(site.shim && site.shim.delete ? { shim: site.shim.delete } : {}), + })), }), - userEvent: "BLOCK" + userEvent: "BLOCK", }), + // This is an option for web extentions which will result in remove items from // memory and notify the web extenion, rather than using the built-in block list. WebExtDismiss: (site, index, eventSource) => ({ @@ -1536,8 +1531,8 @@ const LinkMenuOptions = { action: actionCreators.WebExtEvent(actionTypes.WEBEXT_DISMISS, { source: eventSource, url: site.url, - action_position: index - }) + action_position: index, + }), }), DeleteUrl: (site, index, eventSource, isEnabled, siteInfo) => ({ id: "newtab-menu-delete-history", @@ -1545,77 +1540,74 @@ const LinkMenuOptions = { action: { type: actionTypes.DIALOG_OPEN, data: { - onConfirm: [actionCreators.AlsoToMain({ - type: actionTypes.DELETE_HISTORY_URL, - data: { - url: site.url, - pocket_id: site.pocket_id, - forceBlock: site.bookmarkGuid - } - }), actionCreators.UserEvent(Object.assign({ - event: "DELETE", - source: eventSource, - action_position: index - }, siteInfo))], + onConfirm: [ + actionCreators.AlsoToMain({ + type: actionTypes.DELETE_HISTORY_URL, + data: { + url: site.url, + pocket_id: site.pocket_id, + forceBlock: site.bookmarkGuid, + }, + }), + actionCreators.UserEvent( + Object.assign( + { event: "DELETE", source: eventSource, action_position: index }, + siteInfo + ) + ), + ], eventSource, - body_string_id: ["newtab-confirm-delete-history-p1", "newtab-confirm-delete-history-p2"], + body_string_id: [ + "newtab-confirm-delete-history-p1", + "newtab-confirm-delete-history-p2", + ], confirm_button_string_id: "newtab-topsites-delete-history-button", cancel_button_string_id: "newtab-topsites-cancel-button", - icon: "modal-delete" - } + icon: "modal-delete", + }, }, - userEvent: "DIALOG_OPEN" + userEvent: "DIALOG_OPEN", }), ShowFile: site => ({ id: "newtab-menu-show-file", icon: "search", action: actionCreators.OnlyToMain({ type: actionTypes.SHOW_DOWNLOAD_FILE, - data: { - url: site.url - } - }) + data: { url: site.url }, + }), }), OpenFile: site => ({ id: "newtab-menu-open-file", icon: "open-file", action: actionCreators.OnlyToMain({ type: actionTypes.OPEN_DOWNLOAD_FILE, - data: { - url: site.url - } - }) + data: { url: site.url }, + }), }), CopyDownloadLink: site => ({ id: "newtab-menu-copy-download-link", icon: "copy", action: actionCreators.OnlyToMain({ type: actionTypes.COPY_DOWNLOAD_LINK, - data: { - url: site.url - } - }) + data: { url: site.url }, + }), }), GoToDownloadPage: site => ({ id: "newtab-menu-go-to-download-page", icon: "download", action: actionCreators.OnlyToMain({ type: actionTypes.OPEN_LINK, - data: { - url: site.referrer - } + data: { url: site.referrer }, }), - disabled: !site.referrer + disabled: !site.referrer, }), RemoveDownload: site => ({ id: "newtab-menu-remove-download", icon: "delete", action: actionCreators.OnlyToMain({ type: actionTypes.REMOVE_DOWNLOAD_FILE, - data: { - url: site.url - } - }) + data: { url: site.url }, + }), }), PinTopSite: (site, index) => ({ id: "newtab-menu-pin", @@ -1624,23 +1616,19 @@ const LinkMenuOptions = { type: actionTypes.TOP_SITES_PIN, data: { site, - index - } + index, + }, }), - userEvent: "PIN" + userEvent: "PIN", }), UnpinTopSite: site => ({ id: "newtab-menu-unpin", icon: "unpin", action: actionCreators.AlsoToMain({ type: actionTypes.TOP_SITES_UNPIN, - data: { - site: { - url: site.url - } - } + data: { site: { url: site.url } }, }), - userEvent: "UNPIN" + userEvent: "UNPIN", }), SaveToPocket: (site, index, eventSource = "CARDGRID") => ({ id: "newtab-menu-save-to-pocket", @@ -1648,65 +1636,76 @@ const LinkMenuOptions = { action: actionCreators.AlsoToMain({ type: actionTypes.SAVE_TO_POCKET, data: { - site: { - url: site.url, - title: site.title - } - } + site: { url: site.url, title: site.title }, + }, }), impression: actionCreators.ImpressionStats({ source: eventSource, pocket: 0, - tiles: [{ - id: site.guid, - pos: index, - ...(site.shim && site.shim.save ? { - shim: site.shim.save - } : {}) - }] + tiles: [ + { + id: site.guid, + pos: index, + ...(site.shim && site.shim.save ? { shim: site.shim.save } : {}), + }, + ], }), - userEvent: "SAVE_TO_POCKET" + userEvent: "SAVE_TO_POCKET", }), DeleteFromPocket: site => ({ id: "newtab-menu-delete-pocket", icon: "pocket-delete", action: actionCreators.AlsoToMain({ type: actionTypes.DELETE_FROM_POCKET, - data: { - pocket_id: site.pocket_id - } + data: { pocket_id: site.pocket_id }, }), - userEvent: "DELETE_FROM_POCKET" + userEvent: "DELETE_FROM_POCKET", }), ArchiveFromPocket: site => ({ id: "newtab-menu-archive-pocket", icon: "pocket-archive", action: actionCreators.AlsoToMain({ type: actionTypes.ARCHIVE_FROM_POCKET, - data: { - pocket_id: site.pocket_id - } + data: { pocket_id: site.pocket_id }, }), - userEvent: "ARCHIVE_FROM_POCKET" + userEvent: "ARCHIVE_FROM_POCKET", }), EditTopSite: (site, index) => ({ id: "newtab-menu-edit-topsites", icon: "edit", action: { type: actionTypes.TOP_SITES_EDIT, - data: { - index - } - } + data: { index }, + }, }), - CheckBookmark: site => site.bookmarkGuid ? LinkMenuOptions.RemoveBookmark(site) : LinkMenuOptions.AddBookmark(site), - CheckPinTopSite: (site, index) => site.isPinned ? LinkMenuOptions.UnpinTopSite(site) : LinkMenuOptions.PinTopSite(site, index), - CheckSavedToPocket: (site, index, source) => site.pocket_id ? LinkMenuOptions.DeleteFromPocket(site) : LinkMenuOptions.SaveToPocket(site, index, source), - CheckBookmarkOrArchive: site => site.pocket_id ? LinkMenuOptions.ArchiveFromPocket(site) : LinkMenuOptions.CheckBookmark(site), - CheckArchiveFromPocket: site => site.pocket_id ? LinkMenuOptions.ArchiveFromPocket(site) : LinkMenuOptions.EmptyItem(), - CheckDeleteFromPocket: site => site.pocket_id ? LinkMenuOptions.DeleteFromPocket(site) : LinkMenuOptions.EmptyItem(), - OpenInPrivateWindow: (site, index, eventSource, isEnabled) => isEnabled ? _OpenInPrivateWindow(site) : LinkMenuOptions.EmptyItem() + CheckBookmark: site => + site.bookmarkGuid + ? LinkMenuOptions.RemoveBookmark(site) + : LinkMenuOptions.AddBookmark(site), + CheckPinTopSite: (site, index) => + site.isPinned + ? LinkMenuOptions.UnpinTopSite(site) + : LinkMenuOptions.PinTopSite(site, index), + CheckSavedToPocket: (site, index, source) => + site.pocket_id + ? LinkMenuOptions.DeleteFromPocket(site) + : LinkMenuOptions.SaveToPocket(site, index, source), + CheckBookmarkOrArchive: site => + site.pocket_id + ? LinkMenuOptions.ArchiveFromPocket(site) + : LinkMenuOptions.CheckBookmark(site), + CheckArchiveFromPocket: site => + site.pocket_id + ? LinkMenuOptions.ArchiveFromPocket(site) + : LinkMenuOptions.EmptyItem(), + CheckDeleteFromPocket: site => + site.pocket_id + ? LinkMenuOptions.DeleteFromPocket(site) + : LinkMenuOptions.EmptyItem(), + OpenInPrivateWindow: (site, index, eventSource, isEnabled) => + isEnabled ? _OpenInPrivateWindow(site) : LinkMenuOptions.EmptyItem(), }; + ;// CONCATENATED MODULE: ./content-src/components/LinkMenu/LinkMenu.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, @@ -1927,21 +1926,47 @@ class DSLinkMenu extends (external_React_default()).PureComponent { }))); } } -;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSitesConstants.js +;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSitesConstants.mjs /* 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 TOP_SITES_SOURCE = "TOP_SITES"; -const TOP_SITES_CONTEXT_MENU_OPTIONS = ["CheckPinTopSite", "EditTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"]; -const TOP_SITES_SPOC_CONTEXT_MENU_OPTIONS = ["OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "ShowPrivacyInfo"]; -const TOP_SITES_SPONSORED_POSITION_CONTEXT_MENU_OPTIONS = ["OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "AboutSponsored"]; +const TOP_SITES_CONTEXT_MENU_OPTIONS = [ + "CheckPinTopSite", + "EditTopSite", + "Separator", + "OpenInNewWindow", + "OpenInPrivateWindow", + "Separator", + "BlockUrl", + "DeleteUrl", +]; +const TOP_SITES_SPOC_CONTEXT_MENU_OPTIONS = [ + "OpenInNewWindow", + "OpenInPrivateWindow", + "Separator", + "BlockUrl", + "ShowPrivacyInfo", +]; +const TOP_SITES_SPONSORED_POSITION_CONTEXT_MENU_OPTIONS = [ + "OpenInNewWindow", + "OpenInPrivateWindow", + "Separator", + "BlockUrl", + "AboutSponsored", +]; // the special top site for search shortcut experiment can only have the option to unpin (which removes) the topsite -const TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "BlockUrl"]; +const TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS = [ + "CheckPinTopSite", + "Separator", + "BlockUrl", +]; // minimum size necessary to show a rich icon instead of a screenshot const MIN_RICH_FAVICON_SIZE = 96; // minimum size necessary to show any icon const MIN_SMALL_FAVICON_SIZE = 16; + ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.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, @@ -2033,8 +2058,10 @@ class ImpressionStats_ImpressionStats extends (external_React_default()).PureCom ...(link.shim ? { shim: link.shim } : {}), - recommendation_id: link.recommendation_id - })) + recommendation_id: link.recommendation_id, + fetchTimestamp: link.fetchTimestamp + })), + firstVisibleTimestamp: this.props.firstVisibleTimestamp })); this.impressionCardGuids = cards.map(link => link.id); } @@ -2146,8 +2173,8 @@ class ImpressionStats_ImpressionStats extends (external_React_default()).PureCom } } ImpressionStats_ImpressionStats.defaultProps = { - IntersectionObserver: __webpack_require__.g.IntersectionObserver, - document: __webpack_require__.g.document, + IntersectionObserver: globalThis.IntersectionObserver, + document: globalThis.document, rows: [], source: "" }; @@ -2224,7 +2251,7 @@ class SafeAnchor extends (external_React_default()).PureComponent { }, this.props.children); } } -;// CONCATENATED MODULE: ./content-src/components/Card/types.js +;// CONCATENATED MODULE: ./content-src/components/Card/types.mjs /* 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/. */ @@ -2232,29 +2259,30 @@ class SafeAnchor extends (external_React_default()).PureComponent { const cardContextTypes = { history: { fluentID: "newtab-label-visited", - icon: "history-item" + icon: "history-item", }, removedBookmark: { fluentID: "newtab-label-removed-bookmark", - icon: "bookmark-removed" + icon: "bookmark-removed", }, bookmark: { fluentID: "newtab-label-bookmarked", - icon: "bookmark-added" + icon: "bookmark-added", }, trending: { fluentID: "newtab-label-recommended", - icon: "trending" + icon: "trending", }, pocket: { fluentID: "newtab-label-saved", - icon: "pocket" + icon: "pocket", }, download: { fluentID: "newtab-label-download", - icon: "download" - } + icon: "download", + }, }; + ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.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, @@ -2710,7 +2738,9 @@ class _DSCard extends (external_React_default()).PureComponent { tile_id: this.props.id, ...(this.props.shim && this.props.shim.click ? { shim: this.props.shim.click - } : {}) + } : {}), + fetchTimestamp: this.props.fetchTimestamp, + firstVisibleTimestamp: this.props.firstVisibleTimestamp } })); this.props.dispatch(actionCreators.ImpressionStats({ @@ -2751,7 +2781,9 @@ class _DSCard extends (external_React_default()).PureComponent { tile_id: this.props.id, ...(this.props.shim && this.props.shim.save ? { shim: this.props.shim.save - } : {}) + } : {}), + fetchTimestamp: this.props.fetchTimestamp, + firstVisibleTimestamp: this.props.firstVisibleTimestamp } })); this.props.dispatch(actionCreators.ImpressionStats({ @@ -2913,10 +2945,12 @@ class _DSCard extends (external_React_default()).PureComponent { ...(this.props.shim && this.props.shim.impression ? { shim: this.props.shim.impression } : {}), - recommendation_id: this.props.recommendation_id + recommendation_id: this.props.recommendation_id, + fetchTimestamp: this.props.fetchTimestamp }], dispatch: this.props.dispatch, - source: this.props.type + source: this.props.type, + firstVisibleTimestamp: this.props.firstVisibleTimestamp })), ctaButtonVariant === "variant-b" && /*#__PURE__*/external_React_default().createElement("div", { className: "cta-header" }, "Shop Now"), /*#__PURE__*/external_React_default().createElement(DefaultMeta, { @@ -3273,7 +3307,7 @@ function DSSubHeader({ } function OnboardingExperience({ dispatch, - windowObj = __webpack_require__.g + windowObj = globalThis }) { const [dismissed, setDismissed] = (0,external_React_namespaceObject.useState)(false); const [maxHeight, setMaxHeight] = (0,external_React_namespaceObject.useState)(null); @@ -3549,6 +3583,7 @@ class _CardGrid extends (external_React_default()).PureComponent { url: rec.url, id: rec.id, shim: rec.shim, + fetchTimestamp: rec.fetchTimestamp, type: this.props.type, context: rec.context, sponsor: rec.sponsor, @@ -3564,7 +3599,8 @@ class _CardGrid extends (external_React_default()).PureComponent { ctaButtonSponsors: ctaButtonSponsors, ctaButtonVariant: ctaButtonVariant, spocMessageVariant: spocMessageVariant, - recommendation_id: rec.recommendation_id + recommendation_id: rec.recommendation_id, + firstVisibleTimestamp: this.props.firstVisibleTimestamp })); } if (widgets?.positions?.length && widgets?.data?.length) { @@ -4023,7 +4059,7 @@ class _CollapsibleSection extends (external_React_default()).PureComponent { } } _CollapsibleSection.defaultProps = { - document: __webpack_require__.g.document || { + document: globalThis.document || { addEventListener: () => {}, removeEventListener: () => {}, visibilityState: "hidden" @@ -4111,7 +4147,7 @@ class ModalOverlayWrapper extends (external_React_default()).PureComponent { } } ModalOverlayWrapper.defaultProps = { - document: __webpack_require__.g.document + document: globalThis.document }; ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx /* This Source Code Form is subject to the terms of the Mozilla Public @@ -4443,7 +4479,7 @@ class DSTextPromo extends (external_React_default()).PureComponent { }))); } } -;// CONCATENATED MODULE: ./content-src/lib/screenshot-utils.js +;// CONCATENATED MODULE: ./content-src/lib/screenshot-utils.mjs /* 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/. */ @@ -4462,8 +4498,13 @@ class DSTextPromo extends (external_React_default()).PureComponent { */ const ScreenshotUtils = { isBlob(isLocal, image) { - return !!(image && image.path && (!isLocal && image.data || isLocal && image.url)); + return !!( + image && + image.path && + ((!isLocal && image.data) || (isLocal && image.url)) + ); }, + // This should always be called with a remote image and not a local image. createLocalImageObject(remoteImage) { if (!remoteImage) { @@ -4471,33 +4512,36 @@ const ScreenshotUtils = { } if (this.isBlob(false, remoteImage)) { return { - url: __webpack_require__.g.URL.createObjectURL(remoteImage.data), - path: remoteImage.path + url: globalThis.URL.createObjectURL(remoteImage.data), + path: remoteImage.path, }; } - return { - url: remoteImage - }; + return { url: remoteImage }; }, + // Revokes the object URL of the image if the local image is a blob. // This should always be called with a local image and not a remote image. maybeRevokeBlobObjectURL(localImage) { if (this.isBlob(true, localImage)) { - __webpack_require__.g.URL.revokeObjectURL(localImage.url); + globalThis.URL.revokeObjectURL(localImage.url); } }, + // Checks if remoteImage and localImage are the same. isRemoteImageLocal(localImage, remoteImage) { // Both remoteImage and localImage are present. if (remoteImage && localImage) { - return this.isBlob(false, remoteImage) ? localImage.path === remoteImage.path : localImage.url === remoteImage; + return this.isBlob(false, remoteImage) + ? localImage.path === remoteImage.path + : localImage.url === remoteImage; } // This will only handle the remaining three possible outcomes. // (i.e. everything except when both image and localImage are present) return !remoteImage && !localImage; - } + }, }; + ;// CONCATENATED MODULE: ./content-src/components/Card/Card.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, @@ -4822,14 +4866,13 @@ const PlaceholderCard = props => /*#__PURE__*/external_React_default().createEle placeholder: true, className: props.className }); -;// CONCATENATED MODULE: ./content-src/lib/perf-service.js +;// CONCATENATED MODULE: ./content-src/lib/perf-service.mjs /* 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/. */ - - let usablePerfObj = window.performance; + function _PerfService(options) { // For testing, so that we can use a fake Window.performance object with // known state. @@ -4839,6 +4882,7 @@ function _PerfService(options) { this._perf = usablePerfObj; } } + _PerfService.prototype = { /** * Calls the underlying mark() method on the appropriate Window.performance @@ -4851,6 +4895,7 @@ _PerfService.prototype = { mark: function mark(str) { this._perf.mark(str); }, + /** * Calls the underlying getEntriesByName on the appropriate Window.performance * object. @@ -4859,9 +4904,10 @@ _PerfService.prototype = { * @param {String} type eg "mark" * @return {Array} Performance* objects */ - getEntriesByName: function getEntriesByName(name, type) { - return this._perf.getEntriesByName(name, type); + getEntriesByName: function getEntriesByName(entryName, type) { + return this._perf.getEntriesByName(entryName, type); }, + /** * The timeOrigin property from the appropriate performance object. * Used to ensure that timestamps from the add-on code and the content code @@ -4880,6 +4926,7 @@ _PerfService.prototype = { get timeOrigin() { return this._perf.timeOrigin; }, + /** * Returns the "absolute" version of performance.now(), i.e. one that * should ([bug 1401406](https://bugzilla.mozilla.org/show_bug.cgi?id=1401406) @@ -4890,6 +4937,7 @@ _PerfService.prototype = { absNow: function absNow() { return this.timeOrigin + this._perf.now(); }, + /** * This returns the absolute startTime from the most recent performance.mark() * with the given name. @@ -4908,16 +4956,20 @@ _PerfService.prototype = { * See [bug 1369303](https://bugzilla.mozilla.org/show_bug.cgi?id=1369303) * for more info. */ - getMostRecentAbsMarkStartByName(name) { - let entries = this.getEntriesByName(name, "mark"); + getMostRecentAbsMarkStartByName(entryName) { + let entries = this.getEntriesByName(entryName, "mark"); + if (!entries.length) { - throw new Error(`No marks with the name ${name}`); + throw new Error(`No marks with the name ${entryName}`); } + let mostRecentEntry = entries[entries.length - 1]; return this._perf.timeOrigin + mostRecentEntry.startTime; - } + }, }; + const perfService = new _PerfService(); + ;// CONCATENATED MODULE: ./content-src/components/ComponentPerfTimer/ComponentPerfTimer.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, @@ -5479,6 +5531,9 @@ const INITIAL_STATE = { // Hide the search box after handing off to AwesomeBar and user starts typing. hide: false, }, + Wallpapers: { + wallpaperList: [], + }, }; function App(prevState = INITIAL_STATE.App, action) { @@ -6219,6 +6274,15 @@ function Search(prevState = INITIAL_STATE.Search, action) { } } +function Wallpapers(prevState = INITIAL_STATE.Wallpapers, action) { + switch (action.type) { + case actionTypes.WALLPAPERS_SET: + return { wallpaperList: action.data }; + default: + return prevState; + } +} + const reducers = { TopSites, App, @@ -6230,6 +6294,7 @@ const reducers = { Personalization: Reducers_sys_Personalization, DiscoveryStream, Search, + Wallpapers, }; ;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteFormInput.jsx @@ -6448,8 +6513,8 @@ class TopSiteImpressionWrapper extends (external_React_default()).PureComponent } } TopSiteImpressionWrapper.defaultProps = { - IntersectionObserver: __webpack_require__.g.IntersectionObserver, - document: __webpack_require__.g.document, + IntersectionObserver: globalThis.IntersectionObserver, + document: globalThis.document, actionType: null, tile: null }; @@ -7601,7 +7666,7 @@ class _TopSites extends (external_React_default()).PureComponent { // We hide 2 sites per row when not in the wide layout. let sitesPerRow = TOP_SITES_MAX_SITES_PER_ROW; // $break-point-widest = 1072px (from _variables.scss) - if (!__webpack_require__.g.matchMedia(`(min-width: 1072px)`).matches) { + if (!globalThis.matchMedia(`(min-width: 1072px)`).matches) { sitesPerRow -= 2; } return this.props.TopSites.rows.slice(0, this.props.TopSitesRows * sitesPerRow); @@ -7733,7 +7798,7 @@ class Section extends (external_React_default()).PureComponent { props } = this; let cardsPerRow = CARDS_PER_ROW_DEFAULT; - if (props.compactCards && __webpack_require__.g.matchMedia(`(min-width: 1072px)`).matches) { + if (props.compactCards && globalThis.matchMedia(`(min-width: 1072px)`).matches) { // If the section has compact cards and the viewport is wide enough, we show // 4 columns instead of 3. // $break-point-widest = 1072px (from _variables.scss) @@ -7969,7 +8034,7 @@ class Section extends (external_React_default()).PureComponent { } } Section.defaultProps = { - document: __webpack_require__.g.document, + document: globalThis.document, rows: [], emptyState: {}, pref: {}, @@ -8188,20 +8253,13 @@ class SectionTitle extends (external_React_default()).PureComponent { }, subtitle) : null); } } -;// CONCATENATED MODULE: ./content-src/lib/selectLayoutRender.js +;// CONCATENATED MODULE: ./content-src/lib/selectLayoutRender.mjs /* 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 selectLayoutRender = ({ - state = {}, - prefs = {} -}) => { - const { - layout, - feeds, - spocs - } = state; +const selectLayoutRender = ({ state = {}, prefs = {} }) => { + const { layout, feeds, spocs } = state; let spocIndexPlacementMap = {}; /* This function fills spoc positions on a per placement basis with available spocs. @@ -8210,8 +8268,16 @@ const selectLayoutRender = ({ * If it sees the same placement again, it remembers the previous spoc index, and continues. * If it sees a blocked spoc, it skips that position leaving in a regular story. */ - function fillSpocPositionsForPlacement(data, spocsConfig, spocsData, placementName) { - if (!spocIndexPlacementMap[placementName] && spocIndexPlacementMap[placementName] !== 0) { + function fillSpocPositionsForPlacement( + data, + spocsConfig, + spocsData, + placementName + ) { + if ( + !spocIndexPlacementMap[placementName] && + spocIndexPlacementMap[placementName] !== 0 + ) { spocIndexPlacementMap[placementName] = 0; } const results = [...data]; @@ -8234,107 +8300,154 @@ const selectLayoutRender = ({ results.splice(position.index, 0, spoc); } } + return results; } + const positions = {}; - const DS_COMPONENTS = ["Message", "TextPromo", "SectionTitle", "Signup", "Navigation", "CardGrid", "CollectionCardGrid", "HorizontalRule", "PrivacyLink"]; + const DS_COMPONENTS = [ + "Message", + "TextPromo", + "SectionTitle", + "Signup", + "Navigation", + "CardGrid", + "CollectionCardGrid", + "HorizontalRule", + "PrivacyLink", + ]; + const filterArray = []; + if (!prefs["feeds.topsites"]) { filterArray.push("TopSites"); } - const pocketEnabled = prefs["feeds.section.topstories"] && prefs["feeds.system.topstories"]; + + const pocketEnabled = + prefs["feeds.section.topstories"] && prefs["feeds.system.topstories"]; if (!pocketEnabled) { filterArray.push(...DS_COMPONENTS); } + const placeholderComponent = component => { if (!component.feed) { // TODO we now need a placeholder for topsites and textPromo. return { ...component, data: { - spocs: [] - } + spocs: [], + }, }; } const data = { - recommendations: [] + recommendations: [], }; + let items = 0; if (component.properties && component.properties.items) { items = component.properties.items; } for (let i = 0; i < items; i++) { - data.recommendations.push({ - placeholder: true - }); + data.recommendations.push({ placeholder: true }); } - return { - ...component, - data - }; + + return { ...component, data }; }; // TODO update devtools to show placements const handleSpocs = (data, component) => { let result = [...data]; // Do we ever expect to possibly have a spoc. - if (component.spocs && component.spocs.positions && component.spocs.positions.length) { + if ( + component.spocs && + component.spocs.positions && + component.spocs.positions.length + ) { const placement = component.placement || {}; const placementName = placement.name || "spocs"; const spocsData = spocs.data[placementName]; // We expect a spoc, spocs are loaded, and the server returned spocs. - if (spocs.loaded && spocsData && spocsData.items && spocsData.items.length) { - result = fillSpocPositionsForPlacement(result, component.spocs, spocsData.items, placementName); + if ( + spocs.loaded && + spocsData && + spocsData.items && + spocsData.items.length + ) { + result = fillSpocPositionsForPlacement( + result, + component.spocs, + spocsData.items, + placementName + ); } } return result; }; + const handleComponent = component => { - if (component.spocs && component.spocs.positions && component.spocs.positions.length) { + if ( + component.spocs && + component.spocs.positions && + component.spocs.positions.length + ) { const placement = component.placement || {}; const placementName = placement.name || "spocs"; const spocsData = spocs.data[placementName]; - if (spocs.loaded && spocsData && spocsData.items && spocsData.items.length) { + if ( + spocs.loaded && + spocsData && + spocsData.items && + spocsData.items.length + ) { return { ...component, data: { - spocs: spocsData.items.filter(spoc => spoc && !spocs.blocked.includes(spoc.url)).map((spoc, index) => ({ - ...spoc, - pos: index - })) - } + spocs: spocsData.items + .filter(spoc => spoc && !spocs.blocked.includes(spoc.url)) + .map((spoc, index) => ({ + ...spoc, + pos: index, + })), + }, }; } } return { ...component, data: { - spocs: [] - } + spocs: [], + }, }; }; + const handleComponentWithFeed = component => { positions[component.type] = positions[component.type] || 0; let data = { - recommendations: [] + recommendations: [], }; + const feed = feeds.data[component.feed.url]; if (feed && feed.data) { data = { ...feed.data, - recommendations: [...(feed.data.recommendations || [])] + recommendations: [...(feed.data.recommendations || [])], }; } + if (component && component.properties && component.properties.offset) { data = { ...data, - recommendations: data.recommendations.slice(component.properties.offset) + recommendations: data.recommendations.slice( + component.properties.offset + ), }; } + data = { ...data, - recommendations: handleSpocs(data.recommendations, component) + recommendations: handleSpocs(data.recommendations, component), }; + let items = 0; if (component.properties && component.properties.items) { items = Math.min(component.properties.items, data.recommendations.length); @@ -8346,27 +8459,36 @@ const selectLayoutRender = ({ for (let i = 0; i < items; i++) { data.recommendations[i] = { ...data.recommendations[i], - pos: positions[component.type]++ + pos: positions[component.type]++, }; } - return { - ...component, - data - }; + + return { ...component, data }; }; + const renderLayout = () => { const renderedLayoutArray = []; - for (const row of layout.filter(r => r.components.filter(c => !filterArray.includes(c.type)).length)) { + for (const row of layout.filter( + r => r.components.filter(c => !filterArray.includes(c.type)).length + )) { let components = []; renderedLayoutArray.push({ ...row, - components + components, }); - for (const component of row.components.filter(c => !filterArray.includes(c.type))) { + for (const component of row.components.filter( + c => !filterArray.includes(c.type) + )) { const spocsConfig = component.spocs; if (spocsConfig || component.feed) { // TODO make sure this still works for different loading cases. - if (component.feed && !feeds.data[component.feed.url] || spocsConfig && spocsConfig.positions && spocsConfig.positions.length && !spocs.loaded) { + if ( + (component.feed && !feeds.data[component.feed.url]) || + (spocsConfig && + spocsConfig.positions && + spocsConfig.positions.length && + !spocs.loaded) + ) { components.push(placeholderComponent(component)); return renderedLayoutArray; } @@ -8382,11 +8504,12 @@ const selectLayoutRender = ({ } return renderedLayoutArray; }; + const layoutRender = renderLayout(); - return { - layoutRender - }; + + return { layoutRender }; }; + ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.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, @@ -8528,19 +8651,21 @@ class _DiscoveryStreamBase extends (external_React_default()).PureComponent { privacyNoticeURL: component.properties.privacyNoticeURL }); case "CollectionCardGrid": - const { - DiscoveryStream - } = this.props; - return /*#__PURE__*/external_React_default().createElement(CollectionCardGrid, { - data: component.data, - feed: component.feed, - spocs: DiscoveryStream.spocs, - placement: component.placement, - type: component.type, - items: component.properties.items, - dismissible: this.props.DiscoveryStream.isCollectionDismissible, - dispatch: this.props.dispatch - }); + { + const { + DiscoveryStream + } = this.props; + return /*#__PURE__*/external_React_default().createElement(CollectionCardGrid, { + data: component.data, + feed: component.feed, + spocs: DiscoveryStream.spocs, + placement: component.placement, + type: component.type, + items: component.properties.items, + dismissible: this.props.DiscoveryStream.isCollectionDismissible, + dispatch: this.props.dispatch + }); + } case "CardGrid": return /*#__PURE__*/external_React_default().createElement(CardGrid, { title: component.header && component.header.title, @@ -8561,7 +8686,8 @@ class _DiscoveryStreamBase extends (external_React_default()).PureComponent { spocMessageVariant: component.properties.spocMessageVariant, editorsPicksHeader: component.properties.editorsPicksHeader, recentSavesEnabled: this.props.DiscoveryStream.recentSavesEnabled, - hideDescriptions: this.props.DiscoveryStream.hideDescriptions + hideDescriptions: this.props.DiscoveryStream.hideDescriptions, + firstVisibleTimestamp: this.props.firstVisibleTimestamp }); case "HorizontalRule": return /*#__PURE__*/external_React_default().createElement(HorizontalRule, null); @@ -8718,20 +8844,87 @@ const DiscoveryStreamBase = (0,external_ReactRedux_namespaceObject.connect)(stat DiscoveryStream: state.DiscoveryStream, Prefs: state.Prefs, Sections: state.Sections, - document: __webpack_require__.g.document, + document: globalThis.document, App: state.App }))(_DiscoveryStreamBase); -;// CONCATENATED MODULE: ./content-src/components/CustomizeMenu/BackgroundsSection/BackgroundsSection.jsx +;// CONCATENATED MODULE: ./content-src/components/WallpapersSection/WallpapersSection.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/. */ -class BackgroundsSection extends (external_React_default()).PureComponent { + +class _WallpapersSection extends (external_React_default()).PureComponent { + constructor(props) { + super(props); + this.handleChange = this.handleChange.bind(this); + this.handleReset = this.handleReset.bind(this); + this.prefersHighContrastQuery = null; + this.prefersDarkQuery = null; + } + componentDidMount() { + this.prefersDarkQuery = globalThis.matchMedia("(prefers-color-scheme: dark)"); + } + handleChange(event) { + const { + id + } = event.target; + const prefs = this.props.Prefs.values; + const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; + this.props.setPref(`newtabWallpapers.wallpaper-${colorMode}`, id); + // bug 1892095 + if (prefs["newtabWallpapers.wallpaper-dark"] === "" && colorMode === "light") { + this.props.setPref("newtabWallpapers.wallpaper-dark", id.replace("light", "dark")); + } + if (prefs["newtabWallpapers.wallpaper-light"] === "" && colorMode === "dark") { + this.props.setPref(`newtabWallpapers.wallpaper-light`, id.replace("dark", "light")); + } + } + handleReset() { + const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; + this.props.setPref(`newtabWallpapers.wallpaper-${colorMode}`, ""); + } render() { - return /*#__PURE__*/external_React_default().createElement("div", null); + const { + wallpaperList + } = this.props.Wallpapers; + const { + activeWallpaper + } = this.props; + return /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement("fieldset", { + className: "wallpaper-list" + }, wallpaperList.map(({ + title, + theme, + fluent_id + }) => { + return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("input", { + onChange: this.handleChange, + type: "radio", + name: `wallpaper-${title}`, + id: title, + value: title, + checked: title === activeWallpaper, + "aria-checked": title === activeWallpaper, + className: `wallpaper-input theme-${theme} ${title}` + }), /*#__PURE__*/external_React_default().createElement("label", { + htmlFor: title, + className: "sr-only", + "data-l10n-id": fluent_id + }, fluent_id)); + })), /*#__PURE__*/external_React_default().createElement("button", { + className: "wallpapers-reset", + onClick: this.handleReset, + "data-l10n-id": "newtab-wallpaper-reset" + })); } } +const WallpapersSection = (0,external_ReactRedux_namespaceObject.connect)(state => { + return { + Wallpapers: state.Wallpapers, + Prefs: state.Prefs + }; +})(_WallpapersSection); ;// CONCATENATED MODULE: ./content-src/components/CustomizeMenu/ContentSection/ContentSection.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, @@ -8740,6 +8933,7 @@ class BackgroundsSection extends (external_React_default()).PureComponent { + class ContentSection extends (external_React_default()).PureComponent { constructor(props) { super(props); @@ -8818,7 +9012,10 @@ class ContentSection extends (external_React_default()).PureComponent { mayHaveSponsoredStories, mayHaveRecentSaves, openPreferences, - spocMessageVariant + spocMessageVariant, + wallpapersEnabled, + activeWallpaper, + setPref } = this.props; const { topSitesEnabled, @@ -8831,7 +9028,14 @@ class ContentSection extends (external_React_default()).PureComponent { } = enabledSections; return /*#__PURE__*/external_React_default().createElement("div", { className: "home-section" - }, /*#__PURE__*/external_React_default().createElement("div", { + }, wallpapersEnabled && /*#__PURE__*/external_React_default().createElement("div", { + className: "wallpapers-section" + }, /*#__PURE__*/external_React_default().createElement("h2", { + "data-l10n-id": "newtab-wallpaper-title" + }), /*#__PURE__*/external_React_default().createElement(WallpapersSection, { + setPref: setPref, + activeWallpaper: activeWallpaper + })), /*#__PURE__*/external_React_default().createElement("div", { id: "shortcuts-section", className: "section" }, /*#__PURE__*/external_React_default().createElement("moz-toggle", { @@ -8979,7 +9183,6 @@ class ContentSection extends (external_React_default()).PureComponent { - class _CustomizeMenu extends (external_React_default()).PureComponent { constructor(props) { super(props); @@ -9023,10 +9226,12 @@ class _CustomizeMenu extends (external_React_default()).PureComponent { className: "close-button", "data-l10n-id": "newtab-custom-close-button", ref: c => this.closeButton = c - }), /*#__PURE__*/external_React_default().createElement(BackgroundsSection, null), /*#__PURE__*/external_React_default().createElement(ContentSection, { + }), /*#__PURE__*/external_React_default().createElement(ContentSection, { openPreferences: this.props.openPreferences, setPref: this.props.setPref, enabledSections: this.props.enabledSections, + wallpapersEnabled: this.props.wallpapersEnabled, + activeWallpaper: this.props.activeWallpaper, pocketRegion: this.props.pocketRegion, mayHaveSponsoredTopSites: this.props.mayHaveSponsoredTopSites, mayHaveSponsoredStories: this.props.mayHaveSponsoredStories, @@ -9039,44 +9244,46 @@ class _CustomizeMenu extends (external_React_default()).PureComponent { const CustomizeMenu = (0,external_ReactRedux_namespaceObject.connect)(state => ({ DiscoveryStream: state.DiscoveryStream }))(_CustomizeMenu); -;// CONCATENATED MODULE: ./content-src/lib/constants.js +;// CONCATENATED MODULE: ./content-src/lib/constants.mjs /* 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 IS_NEWTAB = __webpack_require__.g.document && __webpack_require__.g.document.documentURI === "about:newtab"; +const IS_NEWTAB = + globalThis.document && globalThis.document.documentURI === "about:newtab"; const NEWTAB_DARK_THEME = { ntp_background: { r: 42, g: 42, b: 46, - a: 1 + a: 1, }, ntp_card_background: { r: 66, g: 65, b: 77, - a: 1 + a: 1, }, ntp_text: { r: 249, g: 249, b: 250, - a: 1 + a: 1, }, sidebar: { r: 56, g: 56, b: 61, - a: 1 + a: 1, }, sidebar_text: { r: 249, g: 249, b: 250, - a: 1 - } + a: 1, + }, }; + ;// CONCATENATED MODULE: ./content-src/components/Search/Search.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, @@ -9258,6 +9465,8 @@ function Base_extends() { Base_extends = Object.assign ? Object.assign.bind() : +const Base_VISIBLE = "visible"; +const Base_VISIBILITY_CHANGE_EVENT = "visibilitychange"; const PrefsButton = ({ onClick, icon @@ -9306,7 +9515,7 @@ class _Base extends (external_React_default()).PureComponent { // If we skipped the about:welcome overlay and removed the CSS classes // we don't want to add them back to the Activity Stream view document.body.classList.contains("inline-onboarding") ? "inline-onboarding" : ""].filter(v => v).join(" "); - __webpack_require__.g.document.body.className = bodyClassName; + globalThis.document.body.className = bodyClassName; } render() { const { @@ -9337,17 +9546,55 @@ class BaseContent extends (external_React_default()).PureComponent { this.handleOnKeyDown = this.handleOnKeyDown.bind(this); this.onWindowScroll = debounce(this.onWindowScroll.bind(this), 5); this.setPref = this.setPref.bind(this); + this.updateWallpaper = this.updateWallpaper.bind(this); + this.prefersDarkQuery = null; + this.handleColorModeChange = this.handleColorModeChange.bind(this); this.state = { - fixedSearch: false + fixedSearch: false, + firstVisibleTimestamp: null, + colorMode: "" }; } + setFirstVisibleTimestamp() { + if (!this.state.firstVisibleTimestamp) { + this.setState({ + firstVisibleTimestamp: Date.now() + }); + } + } componentDidMount() { __webpack_require__.g.addEventListener("scroll", this.onWindowScroll); __webpack_require__.g.addEventListener("keydown", this.handleOnKeyDown); + if (this.props.document.visibilityState === Base_VISIBLE) { + this.setFirstVisibleTimestamp(); + } else { + this._onVisibilityChange = () => { + if (this.props.document.visibilityState === Base_VISIBLE) { + this.setFirstVisibleTimestamp(); + this.props.document.removeEventListener(Base_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); + this._onVisibilityChange = null; + } + }; + this.props.document.addEventListener(Base_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); + } + // track change event to dark/light mode + this.prefersDarkQuery = globalThis.matchMedia("(prefers-color-scheme: dark)"); + this.prefersDarkQuery.addEventListener("change", this.handleColorModeChange); + this.handleColorModeChange(); + } + handleColorModeChange() { + const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; + this.setState({ + colorMode + }); } componentWillUnmount() { + this.prefersDarkQuery?.removeEventListener("change", this.handleColorModeChange); __webpack_require__.g.removeEventListener("scroll", this.onWindowScroll); __webpack_require__.g.removeEventListener("keydown", this.handleOnKeyDown); + if (this._onVisibilityChange) { + this.props.document.removeEventListener(Base_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); + } } onWindowScroll() { const prefs = this.props.Prefs.values; @@ -9396,6 +9643,53 @@ class BaseContent extends (external_React_default()).PureComponent { setPref(pref, value) { this.props.dispatch(actionCreators.SetPref(pref, value)); } + renderWallpaperAttribution() { + const { + wallpaperList + } = this.props.Wallpapers; + const activeWallpaper = this.props.Prefs.values[`newtabWallpapers.wallpaper-${this.state.colorMode}`]; + const selected = wallpaperList.find(wp => wp.title === activeWallpaper); + // make sure a wallpaper is selected and that the attribution also exists + if (!selected?.attribution) { + return null; + } + const { + name, + webpage + } = selected.attribution; + if (activeWallpaper && wallpaperList && name.url) { + return /*#__PURE__*/external_React_default().createElement("p", { + className: `wallpaper-attribution`, + key: name, + "data-l10n-id": "newtab-wallpaper-attribution", + "data-l10n-args": JSON.stringify({ + author_string: name.string, + author_url: name.url, + webpage_string: webpage.string, + webpage_url: webpage.url + }) + }, /*#__PURE__*/external_React_default().createElement("a", { + "data-l10n-name": "name-link", + href: name.url + }, name.string), /*#__PURE__*/external_React_default().createElement("a", { + "data-l10n-name": "webpage-link", + href: webpage.url + }, webpage.string)); + } + return null; + } + async updateWallpaper() { + const prefs = this.props.Prefs.values; + const { + wallpaperList + } = this.props.Wallpapers; + if (wallpaperList) { + const lightWallpaper = wallpaperList.find(wp => wp.title === prefs["newtabWallpapers.wallpaper-light"]) || ""; + 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 || ""})`); + } + } render() { const { props @@ -9408,6 +9702,8 @@ class BaseContent extends (external_React_default()).PureComponent { customizeMenuVisible } = App; const prefs = props.Prefs.values; + const activeWallpaper = prefs[`newtabWallpapers.wallpaper-${this.state.colorMode}`]; + const wallpapersEnabled = prefs["newtabWallpapers.enabled"]; const { pocketConfig } = prefs; @@ -9435,12 +9731,17 @@ class BaseContent extends (external_React_default()).PureComponent { mayHaveSponsoredTopSites } = prefs; const outerClassName = ["outer-wrapper", isDiscoveryStream && pocketEnabled && "ds-outer-wrapper-search-alignment", isDiscoveryStream && "ds-outer-wrapper-breakpoint-override", prefs.showSearch && this.state.fixedSearch && !noSectionsEnabled && "fixed-search", prefs.showSearch && noSectionsEnabled && "only-search", prefs["logowordmark.alwaysVisible"] && "visible-logo"].filter(v => v).join(" "); + if (wallpapersEnabled) { + this.updateWallpaper(); + } return /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement(CustomizeMenu, { onClose: this.closeCustomizationMenu, onOpen: this.openCustomizationMenu, openPreferences: this.openPreferences, setPref: this.setPref, enabledSections: enabledSections, + wallpapersEnabled: wallpapersEnabled, + activeWallpaper: activeWallpaper, pocketRegion: pocketRegion, mayHaveSponsoredTopSites: mayHaveSponsoredTopSites, mayHaveSponsoredStories: mayHaveSponsoredStories, @@ -9460,31 +9761,38 @@ class BaseContent extends (external_React_default()).PureComponent { className: "borderless-error" }, /*#__PURE__*/external_React_default().createElement(DiscoveryStreamBase, { locale: props.App.locale, - mayHaveSponsoredStories: mayHaveSponsoredStories - })) : /*#__PURE__*/external_React_default().createElement(Sections_Sections, null)), /*#__PURE__*/external_React_default().createElement(ConfirmDialog, null)))); + mayHaveSponsoredStories: mayHaveSponsoredStories, + firstVisibleTimestamp: this.state.firstVisibleTimestamp + })) : /*#__PURE__*/external_React_default().createElement(Sections_Sections, null)), /*#__PURE__*/external_React_default().createElement(ConfirmDialog, null), wallpapersEnabled && this.renderWallpaperAttribution()))); } } +BaseContent.defaultProps = { + document: __webpack_require__.g.document +}; const Base = (0,external_ReactRedux_namespaceObject.connect)(state => ({ App: state.App, Prefs: state.Prefs, Sections: state.Sections, DiscoveryStream: state.DiscoveryStream, - Search: state.Search + Search: state.Search, + Wallpapers: state.Wallpapers }))(_Base); -;// CONCATENATED MODULE: ./content-src/lib/detect-user-session-start.js +;// CONCATENATED MODULE: ./content-src/lib/detect-user-session-start.mjs /* 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 detect_user_session_start_VISIBLE = "visible"; const detect_user_session_start_VISIBILITY_CHANGE_EVENT = "visibilitychange"; + class DetectUserSessionStart { constructor(store, options = {}) { this._store = store; // Overrides for testing - this.document = options.document || __webpack_require__.g.document; + this.document = options.document || globalThis.document; this._perfService = options.perfService || perfService; this._onVisibilityChange = this._onVisibilityChange.bind(this); } @@ -9502,7 +9810,10 @@ class DetectUserSessionStart { this._sendEvent(); } else { // If the document is not visible, listen for when it does become visible. - this.document.addEventListener(detect_user_session_start_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); + this.document.addEventListener( + detect_user_session_start_VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); } } @@ -9513,14 +9824,19 @@ class DetectUserSessionStart { */ _sendEvent() { this._perfService.mark("visibility_event_rcvd_ts"); + try { - let visibility_event_rcvd_ts = this._perfService.getMostRecentAbsMarkStartByName("visibility_event_rcvd_ts"); - this._store.dispatch(actionCreators.AlsoToMain({ - type: actionTypes.SAVE_SESSION_PERF_DATA, - data: { - visibility_event_rcvd_ts - } - })); + let visibility_event_rcvd_ts = + this._perfService.getMostRecentAbsMarkStartByName( + "visibility_event_rcvd_ts" + ); + + this._store.dispatch( + actionCreators.AlsoToMain({ + type: actionTypes.SAVE_SESSION_PERF_DATA, + data: { visibility_event_rcvd_ts }, + }) + ); } catch (ex) { // If this failed, it's likely because the `privacy.resistFingerprinting` // pref is true. We should at least not blow up. @@ -9534,13 +9850,17 @@ class DetectUserSessionStart { _onVisibilityChange() { if (this.document.visibilityState === detect_user_session_start_VISIBLE) { this._sendEvent(); - this.document.removeEventListener(detect_user_session_start_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); + this.document.removeEventListener( + detect_user_session_start_VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); } } } + ;// CONCATENATED MODULE: external "Redux" const external_Redux_namespaceObject = Redux; -;// CONCATENATED MODULE: ./content-src/lib/init-store.js +;// CONCATENATED MODULE: ./content-src/lib/init-store.mjs /* 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/. */ @@ -9548,6 +9868,10 @@ const external_Redux_namespaceObject = Redux; /* eslint-env mozilla/remote-page */ +// We disable import checking here as redux is installed via the npm packages +// at the newtab level, rather than in the top-level package.json. +// eslint-disable-next-line import/no-unresolved + const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE"; const OUTGOING_MESSAGE_NAME = "ActivityStream:ContentToMain"; @@ -9572,11 +9896,9 @@ const INCOMING_MESSAGE_NAME = "ActivityStream:MainToContent"; function mergeStateReducer(mainReducer) { return (prevState, action) => { if (action.type === MERGE_STORE_ACTION) { - return { - ...prevState, - ...action.data - }; + return { ...prevState, ...action.data }; } + return mainReducer(prevState, action); }; } @@ -9593,9 +9915,8 @@ const messageMiddleware = () => next => action => { next(action); } }; -const rehydrationMiddleware = ({ - getState -}) => { + +const rehydrationMiddleware = ({ getState }) => { // NB: The parameter here is MiddlewareAPI which looks like a Store and shares // the same getState, so attached properties are accessible from the store. getState.didRehydrate = false; @@ -9604,17 +9925,24 @@ const rehydrationMiddleware = ({ if (getState.didRehydrate || window.__FROM_STARTUP_CACHE__) { // Startup messages can be safely ignored by the about:home document // stored in the startup cache. - if (window.__FROM_STARTUP_CACHE__ && action.meta && action.meta.isStartup) { + if ( + window.__FROM_STARTUP_CACHE__ && + action.meta && + action.meta.isStartup + ) { return null; } return next(action); } + const isMergeStoreAction = action.type === MERGE_STORE_ACTION; const isRehydrationRequest = action.type === actionTypes.NEW_TAB_STATE_REQUEST; + if (isRehydrationRequest) { getState.didRequestInitialState = true; return next(action); } + if (isMergeStoreAction) { getState.didRehydrate = true; return next(action); @@ -9622,16 +9950,20 @@ const rehydrationMiddleware = ({ // If init happened after our request was made, we need to re-request if (getState.didRequestInitialState && action.type === actionTypes.INIT) { - return next(actionCreators.AlsoToMain({ - type: actionTypes.NEW_TAB_STATE_REQUEST - })); + return next(actionCreators.AlsoToMain({ type: actionTypes.NEW_TAB_STATE_REQUEST })); } - if (actionUtils.isBroadcastToContent(action) || actionUtils.isSendToOneContent(action) || actionUtils.isSendToPreloaded(action)) { + + if ( + actionUtils.isBroadcastToContent(action) || + actionUtils.isSendToOneContent(action) || + actionUtils.isSendToPreloaded(action) + ) { // Note that actions received before didRehydrate will not be dispatched // because this could negatively affect preloading and the the state // will be replaced by rehydration anyway. return null; } + return next(action); }; }; @@ -9644,19 +9976,31 @@ const rehydrationMiddleware = ({ * @return {object} A redux store */ function initStore(reducers, initialState) { - const store = (0,external_Redux_namespaceObject.createStore)(mergeStateReducer((0,external_Redux_namespaceObject.combineReducers)(reducers)), initialState, __webpack_require__.g.RPMAddMessageListener && (0,external_Redux_namespaceObject.applyMiddleware)(rehydrationMiddleware, messageMiddleware)); - if (__webpack_require__.g.RPMAddMessageListener) { - __webpack_require__.g.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => { + const store = (0,external_Redux_namespaceObject.createStore)( + mergeStateReducer((0,external_Redux_namespaceObject.combineReducers)(reducers)), + initialState, + globalThis.RPMAddMessageListener && + (0,external_Redux_namespaceObject.applyMiddleware)(rehydrationMiddleware, messageMiddleware) + ); + + if (globalThis.RPMAddMessageListener) { + globalThis.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => { try { store.dispatch(msg.data); } catch (ex) { console.error("Content msg:", msg, "Dispatch error: ", ex); - dump(`Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${ex.stack}`); + dump( + `Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${ + ex.stack + }` + ); } }); } + return store; } + ;// CONCATENATED MODULE: external "ReactDOM" const external_ReactDOM_namespaceObject = ReactDOM; var external_ReactDOM_default = /*#__PURE__*/__webpack_require__.n(external_ReactDOM_namespaceObject); diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-beach.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-beach.avif Binary files differnew file mode 100644 index 0000000000..5b77286079 --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/dark-beach.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-color.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-color.avif Binary files differnew file mode 100644 index 0000000000..a4fc8e2341 --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/dark-color.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-landscape.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-landscape.avif Binary files differnew file mode 100644 index 0000000000..ed22325f00 --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/dark-landscape.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-mountain.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-mountain.avif Binary files differnew file mode 100644 index 0000000000..a704809a12 --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/dark-mountain.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-panda.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-panda.avif Binary files differnew file mode 100644 index 0000000000..decfff669b --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/dark-panda.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-sky.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-sky.avif Binary files differnew file mode 100644 index 0000000000..51eea392ca --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/dark-sky.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/light-beach.avif b/browser/components/newtab/data/content/assets/wallpapers/light-beach.avif Binary files differnew file mode 100644 index 0000000000..b5f7b2ae67 --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/light-beach.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/light-color.avif b/browser/components/newtab/data/content/assets/wallpapers/light-color.avif Binary files differnew file mode 100644 index 0000000000..3366b7aec6 --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/light-color.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/light-landscape.avif b/browser/components/newtab/data/content/assets/wallpapers/light-landscape.avif Binary files differnew file mode 100644 index 0000000000..1776091825 --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/light-landscape.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/light-mountain.avif b/browser/components/newtab/data/content/assets/wallpapers/light-mountain.avif Binary files differnew file mode 100644 index 0000000000..5983c942fc --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/light-mountain.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/light-panda.avif b/browser/components/newtab/data/content/assets/wallpapers/light-panda.avif Binary files differnew file mode 100644 index 0000000000..d20f405e45 --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/light-panda.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/light-sky.avif b/browser/components/newtab/data/content/assets/wallpapers/light-sky.avif Binary files differnew file mode 100644 index 0000000000..f152f00e06 --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/light-sky.avif diff --git a/browser/components/newtab/karma.mc.config.js b/browser/components/newtab/karma.mc.config.js index fa3ac14587..886b19df7b 100644 --- a/browser/components/newtab/karma.mc.config.js +++ b/browser/components/newtab/karma.mc.config.js @@ -158,6 +158,15 @@ module.exports = function (config) { functions: 0, branches: 0, }, + /** + * WallpaperFeed.sys.mjs is tested via an xpcshell test + */ + "lib/WallpaperFeed.sys.mjs": { + statements: 0, + lines: 0, + functions: 0, + branches: 0, + }, "content-src/components/DiscoveryStreamComponents/**/*.jsx": { statements: 90.48, lines: 90.48, @@ -170,6 +179,15 @@ module.exports = function (config) { functions: 60, branches: 50, }, + /** + * WallpaperSection.jsx is tested via an xpcshell test + */ + "content-src/components/WallpapersSection/*.jsx": { + statements: 0, + lines: 0, + functions: 0, + branches: 0, + }, "content-src/components/DiscoveryStreamAdmin/*.jsx": { statements: 0, lines: 0, @@ -211,7 +229,7 @@ module.exports = function (config) { devtool: "inline-source-map", // This resolve config allows us to import with paths relative to the root directory, e.g. "lib/ActivityStream.sys.mjs" resolve: { - extensions: [".js", ".jsx"], + extensions: [".js", ".jsx", ".mjs"], modules: [PATHS.moduleResolveDirectory, "node_modules"], alias: { asrouter: path.join(__dirname, "../asrouter"), @@ -260,7 +278,7 @@ module.exports = function (config) { }, { enforce: "post", - test: /\.js[mx]?$/, + test: /\.js[x]?$/, loader: "@jsdevtools/coverage-istanbul-loader", options: { esModules: true }, include: [ diff --git a/browser/components/newtab/lib/AboutPreferences.sys.mjs b/browser/components/newtab/lib/AboutPreferences.sys.mjs index 33f7ecdaeb..08e0ca422a 100644 --- a/browser/components/newtab/lib/AboutPreferences.sys.mjs +++ b/browser/components/newtab/lib/AboutPreferences.sys.mjs @@ -5,7 +5,7 @@ import { actionTypes as at, actionCreators as ac, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; const HTML_NS = "http://www.w3.org/1999/xhtml"; export const PREFERENCES_LOADED_EVENT = "home-pane-loaded"; diff --git a/browser/components/newtab/lib/ActivityStream.sys.mjs b/browser/components/newtab/lib/ActivityStream.sys.mjs index f46e8aadf0..fa2d011f11 100644 --- a/browser/components/newtab/lib/ActivityStream.sys.mjs +++ b/browser/components/newtab/lib/ActivityStream.sys.mjs @@ -36,6 +36,7 @@ ChromeUtils.defineESModuleGetters(lazy, { TelemetryFeed: "resource://activity-stream/lib/TelemetryFeed.sys.mjs", TopSitesFeed: "resource://activity-stream/lib/TopSitesFeed.sys.mjs", TopStoriesFeed: "resource://activity-stream/lib/TopStoriesFeed.sys.mjs", + WallpaperFeed: "resource://activity-stream/lib/WallpaperFeed.sys.mjs", }); // NB: Eagerly load modules that will be loaded/constructed/initialized in the @@ -43,7 +44,7 @@ ChromeUtils.defineESModuleGetters(lazy, { import { actionCreators as ac, actionTypes as at, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; const REGION_BASIC_CONFIG = "browser.newtabpage.activity-stream.discoverystream.region-basic-config"; @@ -233,6 +234,27 @@ export const PREFS_CONFIG = new Map([ }, ], [ + "newtabWallpapers.enabled", + { + title: "Boolean flag to turn wallpaper functionality on and off", + value: true, + }, + ], + [ + "newtabWallpapers.wallpaper-light", + { + title: "Currently set light wallpaper", + value: "", + }, + ], + [ + "newtabWallpapers.wallpaper-dark", + { + title: "Currently set dark wallpaper", + value: "", + }, + ], + [ "improvesearch.noDefaultSearchTile", { title: "Remove tiles that are the same as the default search", @@ -524,6 +546,12 @@ const FEEDS_DATA = [ title: "Handles new pocket ui for the new tab page", value: true, }, + { + name: "wallpaperfeed", + factory: () => new lazy.WallpaperFeed(), + title: "Handles fetching and managing wallpaper data from RemoteSettings", + value: true, + }, ]; const FEEDS_CONFIG = new Map(); diff --git a/browser/components/newtab/lib/ActivityStreamMessageChannel.sys.mjs b/browser/components/newtab/lib/ActivityStreamMessageChannel.sys.mjs index 5392a421ca..3cb81b4793 100644 --- a/browser/components/newtab/lib/ActivityStreamMessageChannel.sys.mjs +++ b/browser/components/newtab/lib/ActivityStreamMessageChannel.sys.mjs @@ -13,7 +13,7 @@ import { actionCreators as ac, actionTypes as at, actionUtils as au, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; const ABOUT_NEW_TAB_URL = "about:newtab"; diff --git a/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs b/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs index ee08462503..bff9f1e04e 100644 --- a/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs +++ b/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs @@ -26,7 +26,7 @@ const { setTimeout, clearTimeout } = ChromeUtils.importESModule( import { actionTypes as at, actionCreators as ac, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; const CACHE_KEY = "discovery_stream"; const STARTUP_CACHE_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; // 1 week @@ -565,8 +565,8 @@ export class DiscoveryStreamFeed { generateFeedUrl(isBff) { if (isBff) { - return `https://${lazy.NimbusFeatures.saveToPocket.getVariable( - "bffApi" + return `https://${Services.prefs.getStringPref( + "extensions.pocket.bffApi" )}/desktop/v1/recommendations?locale=$locale®ion=$region&count=30`; } return FEED_URL; @@ -986,8 +986,9 @@ export class DiscoveryStreamFeed { }); if (spocsResponse) { + const fetchTimestamp = Date.now(); spocsState = { - lastUpdated: Date.now(), + lastUpdated: fetchTimestamp, spocs: { ...spocsResponse, }, @@ -1050,8 +1051,13 @@ export class DiscoveryStreamFeed { const { data: blockedResults } = this.filterBlocked(capResult); + const { data: spocsWithFetchTimestamp } = this.addFetchTimestamp( + blockedResults, + fetchTimestamp + ); + const { data: scoredResults, personalized } = - await this.scoreItems(blockedResults, "spocs"); + await this.scoreItems(spocsWithFetchTimestamp, "spocs"); spocsState.spocs = { ...spocsState.spocs, @@ -1209,6 +1215,22 @@ export class DiscoveryStreamFeed { return { data }; } + // Add the fetch timestamp property to each spoc returned to communicate how + // old the spoc is in telemetry when it is used by the client + addFetchTimestamp(spocs, fetchTimestamp) { + if (spocs && spocs.length) { + return { + data: spocs.map(s => { + return { + ...s, + fetchTimestamp, + }; + }), + }; + } + return { data: spocs }; + } + // For backwards compatibility, older spoc endpoint don't have flight_id, // but instead had campaign_id we can use // @@ -1334,8 +1356,8 @@ export class DiscoveryStreamFeed { let options = {}; if (this.isBff) { const headers = new Headers(); - const oAuthConsumerKey = lazy.NimbusFeatures.saveToPocket.getVariable( - "oAuthConsumerKeyBff" + const oAuthConsumerKey = Services.prefs.getStringPref( + "extensions.pocket.oAuthConsumerKeyBff" ); headers.append("consumer_key", oAuthConsumerKey); options = { @@ -1768,7 +1790,7 @@ export class DiscoveryStreamFeed { break; // Check if spocs was disabled. Remove them if they were. case PREF_SHOW_SPONSORED: - case PREF_SHOW_SPONSORED_TOPSITES: + case PREF_SHOW_SPONSORED_TOPSITES: { const dispatch = update => this.store.dispatch(ac.BroadcastToContent(update)); // We refresh placements data because one of the spocs were turned off. @@ -1794,6 +1816,7 @@ export class DiscoveryStreamFeed { await this.cache.set("spocs", {}); await this.loadSpocs(dispatch); break; + } } } diff --git a/browser/components/newtab/lib/DownloadsManager.sys.mjs b/browser/components/newtab/lib/DownloadsManager.sys.mjs index a9a57222ee..f6e99e462a 100644 --- a/browser/components/newtab/lib/DownloadsManager.sys.mjs +++ b/browser/components/newtab/lib/DownloadsManager.sys.mjs @@ -2,7 +2,7 @@ * 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 { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs"; +import { actionTypes as at } from "resource://activity-stream/common/Actions.mjs"; const lazy = {}; diff --git a/browser/components/newtab/lib/FaviconFeed.sys.mjs b/browser/components/newtab/lib/FaviconFeed.sys.mjs index a76566d3e8..18c2231f58 100644 --- a/browser/components/newtab/lib/FaviconFeed.sys.mjs +++ b/browser/components/newtab/lib/FaviconFeed.sys.mjs @@ -2,7 +2,7 @@ * 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 { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs"; +import { actionTypes as at } from "resource://activity-stream/common/Actions.mjs"; import { getDomain } from "resource://activity-stream/lib/TippyTopProvider.sys.mjs"; // We use importESModule here instead of static import so that diff --git a/browser/components/newtab/lib/HighlightsFeed.sys.mjs b/browser/components/newtab/lib/HighlightsFeed.sys.mjs index c603b886da..00eb109896 100644 --- a/browser/components/newtab/lib/HighlightsFeed.sys.mjs +++ b/browser/components/newtab/lib/HighlightsFeed.sys.mjs @@ -2,7 +2,7 @@ * 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 { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs"; +import { actionTypes as at } from "resource://activity-stream/common/Actions.mjs"; import { shortURL } from "resource://activity-stream/lib/ShortURL.sys.mjs"; import { diff --git a/browser/components/newtab/lib/NewTabInit.sys.mjs b/browser/components/newtab/lib/NewTabInit.sys.mjs index db30e009ec..768cc29ea4 100644 --- a/browser/components/newtab/lib/NewTabInit.sys.mjs +++ b/browser/components/newtab/lib/NewTabInit.sys.mjs @@ -5,7 +5,7 @@ import { actionCreators as ac, actionTypes as at, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; /** * NewTabInit - A placeholder for now. This will send a copy of the state to all diff --git a/browser/components/newtab/lib/PlacesFeed.sys.mjs b/browser/components/newtab/lib/PlacesFeed.sys.mjs index 70011412f8..85679153bd 100644 --- a/browser/components/newtab/lib/PlacesFeed.sys.mjs +++ b/browser/components/newtab/lib/PlacesFeed.sys.mjs @@ -6,7 +6,7 @@ import { actionCreators as ac, actionTypes as at, actionUtils as au, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; import { shortURL } from "resource://activity-stream/lib/ShortURL.sys.mjs"; diff --git a/browser/components/newtab/lib/PrefsFeed.sys.mjs b/browser/components/newtab/lib/PrefsFeed.sys.mjs index bb2502ac55..4cb41c0421 100644 --- a/browser/components/newtab/lib/PrefsFeed.sys.mjs +++ b/browser/components/newtab/lib/PrefsFeed.sys.mjs @@ -5,7 +5,7 @@ import { actionCreators as ac, actionTypes as at, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; import { Prefs } from "resource://activity-stream/lib/ActivityStreamPrefs.sys.mjs"; // We use importESModule here instead of static import so that diff --git a/browser/components/newtab/lib/RecommendationProvider.sys.mjs b/browser/components/newtab/lib/RecommendationProvider.sys.mjs index 875c90492b..9fd6b71656 100644 --- a/browser/components/newtab/lib/RecommendationProvider.sys.mjs +++ b/browser/components/newtab/lib/RecommendationProvider.sys.mjs @@ -12,7 +12,7 @@ ChromeUtils.defineESModuleGetters(lazy, { import { actionTypes as at, actionCreators as ac, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; const CACHE_KEY = "personalization"; const PREF_PERSONALIZATION_MODEL_KEYS = diff --git a/browser/components/newtab/lib/SectionsManager.sys.mjs b/browser/components/newtab/lib/SectionsManager.sys.mjs index 069ddbb224..a1634e0d47 100644 --- a/browser/components/newtab/lib/SectionsManager.sys.mjs +++ b/browser/components/newtab/lib/SectionsManager.sys.mjs @@ -15,7 +15,7 @@ const { EventEmitter } = ChromeUtils.importESModule( import { actionCreators as ac, actionTypes as at, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; import { getDefaultOptions } from "resource://activity-stream/lib/ActivityStreamStorage.sys.mjs"; const lazy = {}; @@ -389,7 +389,7 @@ export const SectionsManager = { /** * Sets each card in highlights' context menu options based on the card's type. - * (See types.js for a list of types) + * (See types.mjs for a list of types) * * @param rows section rows containing a type for each card */ diff --git a/browser/components/newtab/lib/SystemTickFeed.sys.mjs b/browser/components/newtab/lib/SystemTickFeed.sys.mjs index d87860fab2..fdbbda3ddd 100644 --- a/browser/components/newtab/lib/SystemTickFeed.sys.mjs +++ b/browser/components/newtab/lib/SystemTickFeed.sys.mjs @@ -2,7 +2,7 @@ * 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 { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs"; +import { actionTypes as at } from "resource://activity-stream/common/Actions.mjs"; const lazy = {}; diff --git a/browser/components/newtab/lib/TelemetryFeed.sys.mjs b/browser/components/newtab/lib/TelemetryFeed.sys.mjs index 1a9e9e3d34..6cf4dba4ab 100644 --- a/browser/components/newtab/lib/TelemetryFeed.sys.mjs +++ b/browser/components/newtab/lib/TelemetryFeed.sys.mjs @@ -18,13 +18,13 @@ const { XPCOMUtils } = ChromeUtils.importESModule( // eslint-disable-next-line mozilla/use-static-import const { MESSAGE_TYPE_HASH: msg } = ChromeUtils.importESModule( - "resource:///modules/asrouter/ActorConstants.sys.mjs" + "resource:///modules/asrouter/ActorConstants.mjs" ); import { actionTypes as at, actionUtils as au, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; import { Prefs } from "resource://activity-stream/lib/ActivityStreamPrefs.sys.mjs"; import { classifySite } from "resource://activity-stream/lib/SiteClassifier.sys.mjs"; @@ -454,8 +454,7 @@ export class TelemetryFeed { event = await this.applyCFRPolicy(event); break; case "badge_user_event": - case "whats-new-panel_user_event": - event = await this.applyWhatsNewPolicy(event); + event = await this.applyToolbarBadgePolicy(event); break; case "infobar_user_event": event = await this.applyInfoBarPolicy(event); @@ -509,12 +508,12 @@ export class TelemetryFeed { * Per Bug 1482134, all the metrics for What's New panel use client_id in * all the release channels */ - async applyWhatsNewPolicy(ping) { + async applyToolbarBadgePolicy(ping) { ping.client_id = await this.telemetryClientId; ping.browser_session_id = lazy.browserSessionId; // Attach page info to `event_context` if there is a session associated with this ping delete ping.action; - return { ping, pingType: "whats-new-panel" }; + return { ping, pingType: "toolbar-badge" }; } async applyInfoBarPolicy(ping) { @@ -715,8 +714,16 @@ export class TelemetryFeed { const session = this.sessions.get(au.getPortIdOfSender(action)); switch (action.data?.event) { case "CLICK": { - const { card_type, topic, recommendation_id, tile_id, shim, feature } = - action.data.value ?? {}; + const { + card_type, + topic, + recommendation_id, + tile_id, + shim, + fetchTimestamp, + firstVisibleTimestamp, + feature, + } = action.data.value ?? {}; if ( action.data.source === "POPULAR_TOPICS" || card_type === "topics_widget" @@ -740,6 +747,14 @@ export class TelemetryFeed { }); if (shim) { Glean.pocket.shim.set(shim); + if (fetchTimestamp) { + Glean.pocket.fetchTimestamp.set(fetchTimestamp * 1000); + } + if (firstVisibleTimestamp) { + Glean.pocket.newtabCreationTimestamp.set( + firstVisibleTimestamp * 1000 + ); + } GleanPings.spoc.submit("click"); } } @@ -755,6 +770,16 @@ export class TelemetryFeed { }); if (action.data.value?.shim) { Glean.pocket.shim.set(action.data.value.shim); + if (action.data.value.fetchTimestamp) { + Glean.pocket.fetchTimestamp.set( + action.data.value.fetchTimestamp * 1000 + ); + } + if (action.data.value.newtabCreationTimestamp) { + Glean.pocket.newtabCreationTimestamp.set( + action.data.value.newtabCreationTimestamp * 1000 + ); + } GleanPings.spoc.submit("save"); } break; @@ -976,6 +1001,14 @@ export class TelemetryFeed { }); if (tile.shim) { Glean.pocket.shim.set(tile.shim); + if (tile.fetchTimestamp) { + Glean.pocket.fetchTimestamp.set(tile.fetchTimestamp * 1000); + } + if (data.firstVisibleTimestamp) { + Glean.pocket.newtabCreationTimestamp.set( + data.firstVisibleTimestamp * 1000 + ); + } GleanPings.spoc.submit("impression"); } }); diff --git a/browser/components/newtab/lib/TopSitesFeed.sys.mjs b/browser/components/newtab/lib/TopSitesFeed.sys.mjs index 796211085b..e259253402 100644 --- a/browser/components/newtab/lib/TopSitesFeed.sys.mjs +++ b/browser/components/newtab/lib/TopSitesFeed.sys.mjs @@ -5,7 +5,7 @@ import { actionCreators as ac, actionTypes as at, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; import { TippyTopProvider } from "resource://activity-stream/lib/TippyTopProvider.sys.mjs"; import { insertPinned, diff --git a/browser/components/newtab/lib/TopStoriesFeed.sys.mjs b/browser/components/newtab/lib/TopStoriesFeed.sys.mjs index be030649dd..5986209a1c 100644 --- a/browser/components/newtab/lib/TopStoriesFeed.sys.mjs +++ b/browser/components/newtab/lib/TopStoriesFeed.sys.mjs @@ -5,7 +5,7 @@ import { actionTypes as at, actionCreators as ac, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; import { Prefs } from "resource://activity-stream/lib/ActivityStreamPrefs.sys.mjs"; import { shortURL } from "resource://activity-stream/lib/ShortURL.sys.mjs"; import { SectionsManager } from "resource://activity-stream/lib/SectionsManager.sys.mjs"; diff --git a/browser/components/newtab/lib/WallpaperFeed.sys.mjs b/browser/components/newtab/lib/WallpaperFeed.sys.mjs new file mode 100644 index 0000000000..cb21311ddc --- /dev/null +++ b/browser/components/newtab/lib/WallpaperFeed.sys.mjs @@ -0,0 +1,117 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + Utils: "resource://services-settings/Utils.sys.mjs", +}); + +import { + actionTypes as at, + actionCreators as ac, +} from "resource://activity-stream/common/Actions.mjs"; + +const PREF_WALLPAPERS_ENABLED = + "browser.newtabpage.activity-stream.newtabWallpapers.enabled"; + +export class WallpaperFeed { + constructor() { + this.loaded = false; + this.wallpaperClient = ""; + this.wallpaperDB = ""; + this.baseAttachmentURL = ""; + } + + /** + * This thin wrapper around global.fetch makes it easier for us to write + * automated tests that simulate responses from this fetch. + */ + fetch(...args) { + return fetch(...args); + } + + /** + * This thin wrapper around lazy.RemoteSettings makes it easier for us to write + * automated tests that simulate responses from this fetch. + */ + RemoteSettings(...args) { + return lazy.RemoteSettings(...args); + } + + async wallpaperSetup(isStartup = false) { + const wallpapersEnabled = Services.prefs.getBoolPref( + PREF_WALLPAPERS_ENABLED + ); + + if (wallpapersEnabled) { + if (!this.wallpaperClient) { + this.wallpaperClient = this.RemoteSettings("newtab-wallpapers"); + } + + await this.getBaseAttachment(); + this.wallpaperClient.on("sync", () => this.updateWallpapers()); + this.updateWallpapers(isStartup); + } + } + + async getBaseAttachment() { + if (!this.baseAttachmentURL) { + const SERVER = lazy.Utils.SERVER_URL; + const serverInfo = await ( + await this.fetch(`${SERVER}/`, { + credentials: "omit", + }) + ).json(); + const { base_url } = serverInfo.capabilities.attachments; + this.baseAttachmentURL = base_url; + } + } + + async updateWallpapers(isStartup = false) { + const records = await this.wallpaperClient.get(); + if (!records?.length) { + return; + } + + if (!this.baseAttachmentURL) { + await this.getBaseAttachment(); + } + const wallpapers = records.map(record => { + return { + ...record, + wallpaperUrl: `${this.baseAttachmentURL}${record.attachment.location}`, + }; + }); + + this.store.dispatch( + ac.BroadcastToContent({ + type: at.WALLPAPERS_SET, + data: wallpapers, + meta: { + isStartup, + }, + }) + ); + } + + async onAction(action) { + switch (action.type) { + case at.INIT: + await this.wallpaperSetup(true /* isStartup */); + break; + case at.UNINIT: + break; + case at.SYSTEM_TICK: + break; + case at.PREF_CHANGED: + if (action.data.name === "newtabWallpapers.enabled") { + await this.wallpaperSetup(false /* isStartup */); + } + break; + case at.WALLPAPERS_SET: + break; + } + } +} diff --git a/browser/components/newtab/metrics.yaml b/browser/components/newtab/metrics.yaml index bd74e609ad..c59247ceef 100644 --- a/browser/components/newtab/metrics.yaml +++ b/browser/components/newtab/metrics.yaml @@ -817,6 +817,35 @@ pocket: send_in_pings: - spoc + fetch_timestamp: + type: datetime + lifetime: ping + description: | + Timestamp of when the spoc was fetched by the client + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1887655 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1887655 + notification_emails: + - dmueller@mozilla.com + expires: never + send_in_pings: + - spoc + + newtab_creation_timestamp: + type: datetime + lifetime: ping + description: | + Timestamp of when this instance of the newtab was first visible to the user. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1887655 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1887655 + notification_emails: + - dmueller@mozilla.com + expires: never + send_in_pings: + - spoc messaging_system: event_context_parse_error: @@ -1031,7 +1060,7 @@ messaging_system: type: string description: > Type of event the ping is capturing. - e.g. "cfr", "whats-new-panel", "onboarding" + e.g. "cfr", "onboarding" bugs: - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 data_reviews: diff --git a/browser/components/newtab/test/browser/browser_as_load_location.js b/browser/components/newtab/test/browser/browser_as_load_location.js index f11b6cf503..ce67ede0c6 100644 --- a/browser/components/newtab/test/browser/browser_as_load_location.js +++ b/browser/components/newtab/test/browser/browser_as_load_location.js @@ -8,7 +8,7 @@ */ async function checkNewtabLoads(selector, message) { // simulate a newtab open as a user would - BrowserOpenTab(); + BrowserCommands.openTab(); // wait until the browser loads let browser = gBrowser.selectedBrowser; diff --git a/browser/components/newtab/test/browser/browser_newtab_overrides.js b/browser/components/newtab/test/browser/browser_newtab_overrides.js index 1d4a0c36e3..c876a62c4e 100644 --- a/browser/components/newtab/test/browser/browser_newtab_overrides.js +++ b/browser/components/newtab/test/browser/browser_newtab_overrides.js @@ -82,7 +82,7 @@ add_task(async function override_loads_in_browser() { Assert.ok(AboutNewTab.newTabURLOverridden, "url has been overridden"); // simulate a newtab open as a user would - BrowserOpenTab(); + BrowserCommands.openTab(); let browser = gBrowser.selectedBrowser; await BrowserTestUtils.browserLoaded(browser); @@ -116,7 +116,7 @@ add_task(async function override_blank_loads_in_browser() { Assert.ok(AboutNewTab.newTabURLOverridden, "url has been overridden"); // simulate a newtab open as a user would - BrowserOpenTab(); + BrowserCommands.openTab(); let browser = gBrowser.selectedBrowser; await BrowserTestUtils.browserLoaded(browser); diff --git a/browser/components/newtab/test/schemas/pings.js b/browser/components/newtab/test/schemas/pings.js index fb52602bd4..2a1dd35ec6 100644 --- a/browser/components/newtab/test/schemas/pings.js +++ b/browser/components/newtab/test/schemas/pings.js @@ -1,7 +1,4 @@ -import { - CONTENT_MESSAGE_TYPE, - MAIN_MESSAGE_TYPE, -} from "common/Actions.sys.mjs"; +import { CONTENT_MESSAGE_TYPE, MAIN_MESSAGE_TYPE } from "common/Actions.mjs"; import Joi from "joi-browser"; export const baseKeys = { diff --git a/browser/components/newtab/test/unit/common/Actions.test.js b/browser/components/newtab/test/unit/common/Actions.test.js index 32e417ea3f..af8d18cee8 100644 --- a/browser/components/newtab/test/unit/common/Actions.test.js +++ b/browser/components/newtab/test/unit/common/Actions.test.js @@ -8,7 +8,7 @@ import { MAIN_MESSAGE_TYPE, PRELOAD_MESSAGE_TYPE, UI_CODE, -} from "common/Actions.sys.mjs"; +} from "common/Actions.mjs"; describe("Actions", () => { it("should set globalImportContext to UI_CODE", () => { diff --git a/browser/components/newtab/test/unit/common/Reducers.test.js b/browser/components/newtab/test/unit/common/Reducers.test.js index 7343fc6224..62f6f48353 100644 --- a/browser/components/newtab/test/unit/common/Reducers.test.js +++ b/browser/components/newtab/test/unit/common/Reducers.test.js @@ -11,7 +11,7 @@ const { Search, ASRouter, } = reducers; -import { actionTypes as at } from "common/Actions.sys.mjs"; +import { actionTypes as at } from "common/Actions.mjs"; describe("Reducers", () => { describe("App", () => { diff --git a/browser/components/newtab/test/unit/content-src/components/Base.test.jsx b/browser/components/newtab/test/unit/content-src/components/Base.test.jsx index c764348006..d8d300a3c9 100644 --- a/browser/components/newtab/test/unit/content-src/components/Base.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/Base.test.jsx @@ -8,7 +8,7 @@ import { ErrorBoundary } from "content-src/components/ErrorBoundary/ErrorBoundar import React from "react"; import { Search } from "content-src/components/Search/Search"; import { shallow } from "enzyme"; -import { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; describe("<Base>", () => { let DEFAULT_PROPS = { @@ -21,6 +21,11 @@ describe("<Base>", () => { adminContent: { message: {}, }, + document: { + visibilityState: "visible", + addEventListener: sinon.stub(), + removeEventListener: sinon.stub(), + }, }; it("should render Base component", () => { @@ -76,6 +81,11 @@ describe("<BaseContent>", () => { Sections: [], DiscoveryStream: { config: { enabled: false } }, dispatch: () => {}, + document: { + visibilityState: "visible", + addEventListener: sinon.stub(), + removeEventListener: sinon.stub(), + }, }; it("should render an ErrorBoundary with a Search child", () => { @@ -114,6 +124,73 @@ describe("<BaseContent>", () => { const wrapper = shallow(<BaseContent {...onlySearchProps} />); assert.lengthOf(wrapper.find(".only-search"), 1); }); + + it("should update firstVisibleTimestamp if it is visible immediately with no event listener", () => { + const props = Object.assign({}, DEFAULT_PROPS, { + document: { + visibilityState: "visible", + addEventListener: sinon.spy(), + removeEventListener: sinon.spy(), + }, + }); + + const wrapper = shallow(<BaseContent {...props} />); + assert.notCalled(props.document.addEventListener); + assert.isDefined(wrapper.state("firstVisibleTimestamp")); + }); + it("should attach an event listener for visibility change if it is not visible", () => { + const props = Object.assign({}, DEFAULT_PROPS, { + document: { + visibilityState: "hidden", + addEventListener: sinon.spy(), + removeEventListener: sinon.spy(), + }, + }); + + const wrapper = shallow(<BaseContent {...props} />); + assert.calledWith(props.document.addEventListener, "visibilitychange"); + assert.notExists(wrapper.state("firstVisibleTimestamp")); + }); + it("should remove the event listener for visibility change when unmounted", () => { + const props = Object.assign({}, DEFAULT_PROPS, { + document: { + visibilityState: "hidden", + addEventListener: sinon.spy(), + removeEventListener: sinon.spy(), + }, + }); + + const wrapper = shallow(<BaseContent {...props} />); + const [, listener] = props.document.addEventListener.firstCall.args; + + wrapper.unmount(); + assert.calledWith( + props.document.removeEventListener, + "visibilitychange", + listener + ); + }); + it("should remove the event listener for visibility change after becoming visible", () => { + const listeners = new Set(); + const props = Object.assign({}, DEFAULT_PROPS, { + document: { + visibilityState: "hidden", + addEventListener: (ev, cb) => listeners.add(cb), + removeEventListener: (ev, cb) => listeners.delete(cb), + }, + }); + + const wrapper = shallow(<BaseContent {...props} />); + assert.equal(listeners.size, 1); + assert.notExists(wrapper.state("firstVisibleTimestamp")); + + // Simulate listeners getting called + props.document.visibilityState = "visible"; + listeners.forEach(l => l()); + + assert.equal(listeners.size, 0); + assert.isDefined(wrapper.state("firstVisibleTimestamp")); + }); }); describe("<PrefsButton>", () => { diff --git a/browser/components/newtab/test/unit/content-src/components/Card.test.jsx b/browser/components/newtab/test/unit/content-src/components/Card.test.jsx index 5f07570b2e..f7f065efae 100644 --- a/browser/components/newtab/test/unit/content-src/components/Card.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/Card.test.jsx @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { _Card as Card, PlaceholderCard, diff --git a/browser/components/newtab/test/unit/content-src/components/ComponentPerfTimer.test.jsx b/browser/components/newtab/test/unit/content-src/components/ComponentPerfTimer.test.jsx index baf203947e..fcc1dd0f45 100644 --- a/browser/components/newtab/test/unit/content-src/components/ComponentPerfTimer.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/ComponentPerfTimer.test.jsx @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { ComponentPerfTimer } from "content-src/components/ComponentPerfTimer/ComponentPerfTimer"; import createMockRaf from "mock-raf"; import React from "react"; diff --git a/browser/components/newtab/test/unit/content-src/components/ConfirmDialog.test.jsx b/browser/components/newtab/test/unit/content-src/components/ConfirmDialog.test.jsx index a471c09e66..3befa4403f 100644 --- a/browser/components/newtab/test/unit/content-src/components/ConfirmDialog.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/ConfirmDialog.test.jsx @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { _ConfirmDialog as ConfirmDialog } from "content-src/components/ConfirmDialog/ConfirmDialog"; import React from "react"; import { shallow } from "enzyme"; 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 e1f84f7d84..0407622cf9 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 @@ -1,4 +1,4 @@ -import { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import { ContentSection } from "content-src/components/CustomizeMenu/ContentSection/ContentSection"; import { mount } from "enzyme"; import React from "react"; 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 41849fba3e..7f40b66200 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 @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { DiscoveryStreamAdminInner, CollapseToggle, diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx index 418a731ba1..ffa32bfc3e 100644 --- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx @@ -13,10 +13,7 @@ import { PlaceholderDSCard, } from "content-src/components/DiscoveryStreamComponents/DSCard/DSCard"; import { TopicsWidget } from "content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget"; -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import React from "react"; import { shallow, mount } from "enzyme"; 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 1d572ee3ce..afb6d6dcd2 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 @@ -10,10 +10,7 @@ import { StatusMessage, SponsorLabel, } from "content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter"; -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { DSLinkMenu } from "content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu"; import React from "react"; import { INITIAL_STATE } from "common/Reducers.sys.mjs"; @@ -28,6 +25,8 @@ const DEFAULT_PROPS = { isForStartupCache: false, }, DiscoveryStream: INITIAL_STATE.DiscoveryStream, + fetchTimestamp: new Date("March 20, 2024 10:30:44").getTime(), + firstVisibleTimestamp: new Date("March 21, 2024 10:11:12").getTime(), }; describe("<DSCard>", () => { @@ -174,6 +173,8 @@ describe("<DSCard>", () => { card_type: "organic", recommendation_id: undefined, tile_id: "fooidx", + fetchTimestamp: DEFAULT_PROPS.fetchTimestamp, + firstVisibleTimestamp: DEFAULT_PROPS.firstVisibleTimestamp, }, }) ); @@ -212,6 +213,8 @@ describe("<DSCard>", () => { card_type: "spoc", recommendation_id: undefined, tile_id: "fooidx", + fetchTimestamp: DEFAULT_PROPS.fetchTimestamp, + firstVisibleTimestamp: DEFAULT_PROPS.firstVisibleTimestamp, }, }) ); @@ -258,6 +261,8 @@ describe("<DSCard>", () => { recommendation_id: undefined, tile_id: "fooidx", shim: "click shim", + fetchTimestamp: DEFAULT_PROPS.fetchTimestamp, + firstVisibleTimestamp: DEFAULT_PROPS.firstVisibleTimestamp, }, }) ); @@ -370,7 +375,12 @@ describe("<DSCard>", () => { describe("DSCard onSaveClick", () => { it("should fire telemetry for onSaveClick", () => { - wrapper.setProps({ id: "fooidx", pos: 1, type: "foo" }); + wrapper.setProps({ + id: "fooidx", + pos: 1, + type: "foo", + fetchTimestamp: undefined, + }); wrapper.instance().onSaveClick(); assert.calledThrice(dispatch); @@ -391,6 +401,8 @@ describe("<DSCard>", () => { card_type: "organic", recommendation_id: undefined, tile_id: "fooidx", + fetchTimestamp: undefined, + firstVisibleTimestamp: DEFAULT_PROPS.firstVisibleTimestamp, }, }) ); diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx index 08ac7868ce..a18e688758 100644 --- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx @@ -5,7 +5,7 @@ import { } from "content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter"; import React from "react"; import { mount } from "enzyme"; -import { cardContextTypes } from "content-src/components/Card/types.js"; +import { cardContextTypes } from "content-src/components/Card/types.mjs"; import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText.jsx"; describe("<DSContextFooter>", () => { diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx index b4b743c7ff..b5acbf3b56 100644 --- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx @@ -1,6 +1,6 @@ import { DSPrivacyModal } from "content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal"; import { shallow, mount } from "enzyme"; -import { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import React from "react"; describe("Discovery Stream <DSPrivacyModal>", () => { diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx index 4926cc6c70..c935acde1a 100644 --- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx @@ -2,7 +2,7 @@ import { ImpressionStats, INTERSECTION_RATIO, } from "content-src/components/DiscoveryStreamImpressionStats/ImpressionStats"; -import { actionTypes as at } from "common/Actions.sys.mjs"; +import { actionTypes as at } from "common/Actions.mjs"; import React from "react"; import { shallow } from "enzyme"; @@ -33,12 +33,15 @@ describe("<ImpressionStats>", () => { }; } + const TEST_FETCH_TIMESTAMP = Date.now(); + const TEST_FIRST_VISIBLE_TIMESTAMP = Date.now(); const DEFAULT_PROPS = { rows: [ - { id: 1, pos: 0 }, - { id: 2, pos: 1 }, - { id: 3, pos: 2 }, + { id: 1, pos: 0, fetchTimestamp: TEST_FETCH_TIMESTAMP }, + { id: 2, pos: 1, fetchTimestamp: TEST_FETCH_TIMESTAMP }, + { id: 3, pos: 2, fetchTimestamp: TEST_FETCH_TIMESTAMP }, ], + firstVisibleTimestamp: TEST_FIRST_VISIBLE_TIMESTAMP, source: SOURCE, IntersectionObserver: buildIntersectionObserver(FullIntersectEntries), document: { @@ -76,7 +79,7 @@ describe("<ImpressionStats>", () => { assert.notCalled(dispatch); }); - it("should noly send loaded content but not impression when the wrapped item is not visbible", () => { + it("should only send loaded content but not impression when the wrapped item is not visbible", () => { const dispatch = sinon.spy(); const props = { dispatch, @@ -128,11 +131,37 @@ describe("<ImpressionStats>", () => { [action] = dispatch.secondCall.args; assert.equal(action.type, at.DISCOVERY_STREAM_IMPRESSION_STATS); assert.equal(action.data.source, SOURCE); + assert.equal( + action.data.firstVisibleTimestamp, + TEST_FIRST_VISIBLE_TIMESTAMP + ); assert.deepEqual(action.data.tiles, [ - { id: 1, pos: 0, type: "organic", recommendation_id: undefined }, - { id: 2, pos: 1, type: "organic", recommendation_id: undefined }, - { id: 3, pos: 2, type: "organic", recommendation_id: undefined }, + { + id: 1, + pos: 0, + type: "organic", + recommendation_id: undefined, + fetchTimestamp: TEST_FETCH_TIMESTAMP, + }, + { + id: 2, + pos: 1, + type: "organic", + recommendation_id: undefined, + fetchTimestamp: TEST_FETCH_TIMESTAMP, + }, + { + id: 3, + pos: 2, + type: "organic", + recommendation_id: undefined, + fetchTimestamp: TEST_FETCH_TIMESTAMP, + }, ]); + assert.equal( + action.data.firstVisibleTimestamp, + TEST_FIRST_VISIBLE_TIMESTAMP + ); }); it("should send a DISCOVERY_STREAM_SPOC_IMPRESSION when the wrapped item has a flightId", () => { const dispatch = sinon.spy(); @@ -207,10 +236,32 @@ describe("<ImpressionStats>", () => { [action] = dispatch.firstCall.args; assert.equal(action.type, at.DISCOVERY_STREAM_IMPRESSION_STATS); assert.deepEqual(action.data.tiles, [ - { id: 1, pos: 0, type: "organic", recommendation_id: undefined }, - { id: 2, pos: 1, type: "organic", recommendation_id: undefined }, - { id: 3, pos: 2, type: "organic", recommendation_id: undefined }, + { + id: 1, + pos: 0, + type: "organic", + recommendation_id: undefined, + fetchTimestamp: TEST_FETCH_TIMESTAMP, + }, + { + id: 2, + pos: 1, + type: "organic", + recommendation_id: undefined, + fetchTimestamp: TEST_FETCH_TIMESTAMP, + }, + { + id: 3, + pos: 2, + type: "organic", + recommendation_id: undefined, + fetchTimestamp: TEST_FETCH_TIMESTAMP, + }, ]); + assert.equal( + action.data.firstVisibleTimestamp, + TEST_FIRST_VISIBLE_TIMESTAMP + ); }); it("should remove visibility change listener when the wrapper is removed", () => { const props = { diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx index f879600a8f..5c9dcb4c14 100644 --- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx @@ -6,10 +6,7 @@ import { TopicsWidget, } from "content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget"; import { SafeAnchor } from "content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor"; -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { mount } from "enzyme"; import React from "react"; diff --git a/browser/components/newtab/test/unit/content-src/components/Sections.test.jsx b/browser/components/newtab/test/unit/content-src/components/Sections.test.jsx index 9f4008369a..69d023c668 100644 --- a/browser/components/newtab/test/unit/content-src/components/Sections.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/Sections.test.jsx @@ -5,7 +5,7 @@ import { SectionIntl, _Sections as Sections, } from "content-src/components/Sections/Sections"; -import { actionTypes as at } from "common/Actions.sys.mjs"; +import { actionTypes as at } from "common/Actions.mjs"; import { mount, shallow } from "enzyme"; import { PlaceholderCard } from "content-src/components/Card/Card"; import { PocketLoggedInCta } from "content-src/components/PocketLoggedInCta/PocketLoggedInCta"; diff --git a/browser/components/newtab/test/unit/content-src/components/TopSites.test.jsx b/browser/components/newtab/test/unit/content-src/components/TopSites.test.jsx index 798bb9b8c7..9797a4863e 100644 --- a/browser/components/newtab/test/unit/content-src/components/TopSites.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/TopSites.test.jsx @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { GlobalOverrider } from "test/unit/utils"; import { MIN_RICH_FAVICON_SIZE } from "content-src/components/TopSites/TopSitesConstants"; import { diff --git a/browser/components/newtab/test/unit/content-src/components/TopSites/TopSiteImpressionWrapper.test.jsx b/browser/components/newtab/test/unit/content-src/components/TopSites/TopSiteImpressionWrapper.test.jsx index 3f7e725de0..b1b501ca44 100644 --- a/browser/components/newtab/test/unit/content-src/components/TopSites/TopSiteImpressionWrapper.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/TopSites/TopSiteImpressionWrapper.test.jsx @@ -2,7 +2,7 @@ import { TopSiteImpressionWrapper, INTERSECTION_RATIO, } from "content-src/components/TopSites/TopSiteImpressionWrapper"; -import { actionTypes as at } from "common/Actions.sys.mjs"; +import { actionTypes as at } from "common/Actions.mjs"; import React from "react"; import { shallow } from "enzyme"; diff --git a/browser/components/newtab/test/unit/content-src/lib/detect-user-session-start.test.js b/browser/components/newtab/test/unit/content-src/lib/detect-user-session-start.test.js index 5a7fad7cc0..3629bb7a68 100644 --- a/browser/components/newtab/test/unit/content-src/lib/detect-user-session-start.test.js +++ b/browser/components/newtab/test/unit/content-src/lib/detect-user-session-start.test.js @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { DetectUserSessionStart } from "content-src/lib/detect-user-session-start"; describe("detectUserSessionStart", () => { diff --git a/browser/components/newtab/test/unit/content-src/lib/init-store.test.js b/browser/components/newtab/test/unit/content-src/lib/init-store.test.js index 0dd510ef1a..8f998b64d0 100644 --- a/browser/components/newtab/test/unit/content-src/lib/init-store.test.js +++ b/browser/components/newtab/test/unit/content-src/lib/init-store.test.js @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { addNumberReducer, GlobalOverrider } from "test/unit/utils"; import { INCOMING_MESSAGE_NAME, diff --git a/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js b/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js index 233f31b6ca..fb28c9490b 100644 --- a/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js +++ b/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js @@ -1,5 +1,5 @@ import { combineReducers, createStore } from "redux"; -import { actionTypes as at } from "common/Actions.sys.mjs"; +import { actionTypes as at } from "common/Actions.mjs"; import { GlobalOverrider } from "test/unit/utils"; import { reducers } from "common/Reducers.sys.mjs"; import { selectLayoutRender } from "content-src/lib/selectLayoutRender"; diff --git a/browser/components/newtab/test/unit/lib/AboutPreferences.test.js b/browser/components/newtab/test/unit/lib/AboutPreferences.test.js index 20765608fa..a19bf698d9 100644 --- a/browser/components/newtab/test/unit/lib/AboutPreferences.test.js +++ b/browser/components/newtab/test/unit/lib/AboutPreferences.test.js @@ -3,10 +3,7 @@ import { AboutPreferences, PREFERENCES_LOADED_EVENT, } from "lib/AboutPreferences.sys.mjs"; -import { - actionTypes as at, - actionCreators as ac, -} from "common/Actions.sys.mjs"; +import { actionTypes as at, actionCreators as ac } from "common/Actions.mjs"; import { GlobalOverrider } from "test/unit/utils"; describe("AboutPreferences Feed", () => { diff --git a/browser/components/newtab/test/unit/lib/ActivityStream.test.js b/browser/components/newtab/test/unit/lib/ActivityStream.test.js index b9deba1069..7921ae2c91 100644 --- a/browser/components/newtab/test/unit/lib/ActivityStream.test.js +++ b/browser/components/newtab/test/unit/lib/ActivityStream.test.js @@ -1,4 +1,4 @@ -import { CONTENT_MESSAGE_TYPE } from "common/Actions.sys.mjs"; +import { CONTENT_MESSAGE_TYPE } from "common/Actions.mjs"; import { ActivityStream, PREFS_CONFIG } from "lib/ActivityStream.sys.mjs"; import { GlobalOverrider } from "test/unit/utils"; diff --git a/browser/components/newtab/test/unit/lib/ActivityStreamMessageChannel.test.js b/browser/components/newtab/test/unit/lib/ActivityStreamMessageChannel.test.js index 4bea86331d..8df62b2903 100644 --- a/browser/components/newtab/test/unit/lib/ActivityStreamMessageChannel.test.js +++ b/browser/components/newtab/test/unit/lib/ActivityStreamMessageChannel.test.js @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { ActivityStreamMessageChannel, DEFAULT_OPTIONS, @@ -16,7 +13,7 @@ const OPTIONS = [ ]; // Create an object containing details about a tab as expected within -// the loaded tabs map in ActivityStreamMessageChannel.jsm. +// the loaded tabs map in ActivityStreamMessageChannel.sys.mjs. function getTabDetails(portID, url = "about:newtab", extraArgs = {}) { let actor = { portID, diff --git a/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js b/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js index 92e10facb3..e10a4cbc04 100644 --- a/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js +++ b/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js @@ -2,7 +2,7 @@ import { actionCreators as ac, actionTypes as at, actionUtils as au, -} from "common/Actions.sys.mjs"; +} from "common/Actions.mjs"; import { combineReducers, createStore } from "redux"; import { GlobalOverrider } from "test/unit/utils"; import { DiscoveryStreamFeed } from "lib/DiscoveryStreamFeed.sys.mjs"; @@ -849,6 +849,8 @@ describe("DiscoveryStreamFeed", () => { spocs: { items: [{ id: "data" }] }, }); sandbox.stub(feed.cache, "set").returns(Promise.resolve()); + const loadTimestamp = 100; + clock.tick(loadTimestamp); await feed.loadSpocs(feed.store.dispatch); @@ -860,15 +862,15 @@ describe("DiscoveryStreamFeed", () => { title: "", sponsor: "", sponsored_by_override: undefined, - items: [{ id: "data", score: 1 }], + items: [{ id: "data", score: 1, fetchTimestamp: loadTimestamp }], }, }, - lastUpdated: 0, + lastUpdated: loadTimestamp, }); assert.deepEqual( feed.store.getState().DiscoveryStream.spocs.data.spocs.items[0], - { id: "data", score: 1 } + { id: "data", score: 1, fetchTimestamp: loadTimestamp } ); }); it("should normalizeSpocsItems for older spoc data", async () => { @@ -882,7 +884,7 @@ describe("DiscoveryStreamFeed", () => { assert.deepEqual( feed.store.getState().DiscoveryStream.spocs.data.spocs.items[0], - { id: "data", score: 1 } + { id: "data", score: 1, fetchTimestamp: 0 } ); }); it("should dispatch DISCOVERY_STREAM_PERSONALIZATION_OVERRIDE with feature_flags", async () => { @@ -936,7 +938,7 @@ describe("DiscoveryStreamFeed", () => { context: "", sponsor: "", sponsored_by_override: undefined, - items: [{ id: "data", score: 1 }], + items: [{ id: "data", score: 1, fetchTimestamp: 0 }], }, placement2: { title: "", @@ -978,7 +980,7 @@ describe("DiscoveryStreamFeed", () => { context: "context", sponsor: "", sponsored_by_override: undefined, - items: [{ id: "data", score: 1 }], + items: [{ id: "data", score: 1, fetchTimestamp: 0 }], }, }); }); @@ -3444,16 +3446,12 @@ describe("DiscoveryStreamFeed", () => { }, }); sandbox.stub(global.Region, "home").get(() => "DE"); - globals.set("NimbusFeatures", { - saveToPocket: { - getVariable: sandbox.stub(), - }, - }); - global.NimbusFeatures.saveToPocket.getVariable - .withArgs("bffApi") + sandbox.stub(global.Services.prefs, "getStringPref"); + global.Services.prefs.getStringPref + .withArgs("extensions.pocket.bffApi") .returns("bffApi"); - global.NimbusFeatures.saveToPocket.getVariable - .withArgs("oAuthConsumerKeyBff") + global.Services.prefs.getStringPref + .withArgs("extensions.pocket.oAuthConsumerKeyBff") .returns("oAuthConsumerKeyBff"); }); it("should return true with isBff", async () => { diff --git a/browser/components/newtab/test/unit/lib/DownloadsManager.test.js b/browser/components/newtab/test/unit/lib/DownloadsManager.test.js index ac262baf90..5e2979893d 100644 --- a/browser/components/newtab/test/unit/lib/DownloadsManager.test.js +++ b/browser/components/newtab/test/unit/lib/DownloadsManager.test.js @@ -1,4 +1,4 @@ -import { actionTypes as at } from "common/Actions.sys.mjs"; +import { actionTypes as at } from "common/Actions.mjs"; import { DownloadsManager } from "lib/DownloadsManager.sys.mjs"; import { GlobalOverrider } from "test/unit/utils"; diff --git a/browser/components/newtab/test/unit/lib/FaviconFeed.test.js b/browser/components/newtab/test/unit/lib/FaviconFeed.test.js index e9be9b86ba..8b9cf24984 100644 --- a/browser/components/newtab/test/unit/lib/FaviconFeed.test.js +++ b/browser/components/newtab/test/unit/lib/FaviconFeed.test.js @@ -1,6 +1,6 @@ "use strict"; import { FaviconFeed, fetchIconFromRedirects } from "lib/FaviconFeed.sys.mjs"; -import { actionTypes as at } from "common/Actions.sys.mjs"; +import { actionTypes as at } from "common/Actions.mjs"; import { GlobalOverrider } from "test/unit/utils"; const FAKE_ENDPOINT = "https://foo.com/"; diff --git a/browser/components/newtab/test/unit/lib/NewTabInit.test.js b/browser/components/newtab/test/unit/lib/NewTabInit.test.js index 68ab9d7821..0def9293f0 100644 --- a/browser/components/newtab/test/unit/lib/NewTabInit.test.js +++ b/browser/components/newtab/test/unit/lib/NewTabInit.test.js @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { NewTabInit } from "lib/NewTabInit.sys.mjs"; describe("NewTabInit", () => { diff --git a/browser/components/newtab/test/unit/lib/PrefsFeed.test.js b/browser/components/newtab/test/unit/lib/PrefsFeed.test.js index 498c7198ab..8f33dce24f 100644 --- a/browser/components/newtab/test/unit/lib/PrefsFeed.test.js +++ b/browser/components/newtab/test/unit/lib/PrefsFeed.test.js @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { GlobalOverrider } from "test/unit/utils"; import { PrefsFeed } from "lib/PrefsFeed.sys.mjs"; diff --git a/browser/components/newtab/test/unit/lib/RecommendationProvider.test.js b/browser/components/newtab/test/unit/lib/RecommendationProvider.test.js index 9e68f4869a..05999be08d 100644 --- a/browser/components/newtab/test/unit/lib/RecommendationProvider.test.js +++ b/browser/components/newtab/test/unit/lib/RecommendationProvider.test.js @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { RecommendationProvider } from "lib/RecommendationProvider.sys.mjs"; import { combineReducers, createStore } from "redux"; import { reducers } from "common/Reducers.sys.mjs"; diff --git a/browser/components/newtab/test/unit/lib/SectionsManager.test.js b/browser/components/newtab/test/unit/lib/SectionsManager.test.js index b3a9abd70c..45c5b7c689 100644 --- a/browser/components/newtab/test/unit/lib/SectionsManager.test.js +++ b/browser/components/newtab/test/unit/lib/SectionsManager.test.js @@ -5,7 +5,7 @@ import { CONTENT_MESSAGE_TYPE, MAIN_MESSAGE_TYPE, PRELOAD_MESSAGE_TYPE, -} from "common/Actions.sys.mjs"; +} from "common/Actions.mjs"; import { EventEmitter, GlobalOverrider } from "test/unit/utils"; import { SectionsFeed, SectionsManager } from "lib/SectionsManager.sys.mjs"; diff --git a/browser/components/newtab/test/unit/lib/SystemTickFeed.test.js b/browser/components/newtab/test/unit/lib/SystemTickFeed.test.js index a0789b182e..f5ba73d2ea 100644 --- a/browser/components/newtab/test/unit/lib/SystemTickFeed.test.js +++ b/browser/components/newtab/test/unit/lib/SystemTickFeed.test.js @@ -2,7 +2,7 @@ import { SYSTEM_TICK_INTERVAL, SystemTickFeed, } from "lib/SystemTickFeed.sys.mjs"; -import { actionTypes as at } from "common/Actions.sys.mjs"; +import { actionTypes as at } from "common/Actions.mjs"; import { GlobalOverrider } from "test/unit/utils"; describe("System Tick Feed", () => { diff --git a/browser/components/newtab/test/xpcshell/test_HighlightsFeed.js b/browser/components/newtab/test/xpcshell/test_HighlightsFeed.js index 31a03947cd..1cb8a44631 100644 --- a/browser/components/newtab/test/xpcshell/test_HighlightsFeed.js +++ b/browser/components/newtab/test/xpcshell/test_HighlightsFeed.js @@ -4,7 +4,7 @@ "use strict"; const { actionTypes: at } = ChromeUtils.importESModule( - "resource://activity-stream/common/Actions.sys.mjs" + "resource://activity-stream/common/Actions.mjs" ); ChromeUtils.defineESModuleGetters(this, { diff --git a/browser/components/newtab/test/xpcshell/test_PlacesFeed.js b/browser/components/newtab/test/xpcshell/test_PlacesFeed.js index 19f9e343f5..78dda7818e 100644 --- a/browser/components/newtab/test/xpcshell/test_PlacesFeed.js +++ b/browser/components/newtab/test/xpcshell/test_PlacesFeed.js @@ -4,7 +4,7 @@ "use strict"; const { actionTypes: at, actionCreators: ac } = ChromeUtils.importESModule( - "resource://activity-stream/common/Actions.sys.mjs" + "resource://activity-stream/common/Actions.mjs" ); ChromeUtils.defineESModuleGetters(this, { diff --git a/browser/components/newtab/test/xpcshell/test_TelemetryFeed.js b/browser/components/newtab/test/xpcshell/test_TelemetryFeed.js index 59d82f5583..354eac8c2a 100644 --- a/browser/components/newtab/test/xpcshell/test_TelemetryFeed.js +++ b/browser/components/newtab/test/xpcshell/test_TelemetryFeed.js @@ -4,11 +4,11 @@ "use strict"; const { actionCreators: ac, actionTypes: at } = ChromeUtils.importESModule( - "resource://activity-stream/common/Actions.sys.mjs" + "resource://activity-stream/common/Actions.mjs" ); const { MESSAGE_TYPE_HASH: msg } = ChromeUtils.importESModule( - "resource:///modules/asrouter/ActorConstants.sys.mjs" + "resource:///modules/asrouter/ActorConstants.mjs" ); const { updateAppInfo } = ChromeUtils.importESModule( @@ -947,18 +947,18 @@ add_task( } ); -add_task(async function test_applyWhatsNewPolicy() { +add_task(async function test_applyToolbarBadgePolicy() { info( - "TelemetryFeed.applyWhatsNewPolicy should set client_id and set pingType" + "TelemetryFeed.applyToolbarBadgePolicy should set client_id and set pingType" ); let instance = new TelemetryFeed(); - let { ping, pingType } = await instance.applyWhatsNewPolicy({}); + let { ping, pingType } = await instance.applyToolbarBadgePolicy({}); Assert.equal( ping.client_id, Services.prefs.getCharPref("toolkit.telemetry.cachedClientID") ); - Assert.equal(pingType, "whats-new-panel"); + Assert.equal(pingType, "toolbar-badge"); }); add_task(async function test_applyInfoBarPolicy() { @@ -1288,10 +1288,10 @@ add_task(async function test_createASRouterEvent_call_correctPolicy() { message_id: "onboarding_message_01", }); - testCallCorrectPolicy("applyWhatsNewPolicy", { - action: "whats-new-panel_user_event", - event: "CLICK_BUTTON", - message_id: "whats-new-panel_message_01", + testCallCorrectPolicy("applyToolbarBadgePolicy", { + action: "badge_user_event", + event: "IMPRESSION", + message_id: "badge_message_01", }); testCallCorrectPolicy("applyMomentsPolicy", { @@ -2230,6 +2230,8 @@ add_task( const POS_1 = 1; const POS_2 = 4; const SHIM = "Y29uc2lkZXIgeW91ciBjdXJpb3NpdHkgcmV3YXJkZWQ="; + const FETCH_TIMESTAMP = new Date("March 22, 2024 10:15:20"); + const NEWTAB_CREATION_TIMESTAMP = new Date("March 23, 2024 11:10:30"); sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); let pingSubmitted = new Promise(resolve => { @@ -2252,6 +2254,14 @@ add_task( tile_id: String(2), }); Assert.equal(Glean.pocket.shim.testGetValue(), SHIM); + Assert.deepEqual( + Glean.pocket.fetchTimestamp.testGetValue(), + FETCH_TIMESTAMP + ); + Assert.deepEqual( + Glean.pocket.newtabCreationTimestamp.testGetValue(), + NEWTAB_CREATION_TIMESTAMP + ); resolve(); }); @@ -2272,10 +2282,12 @@ add_task( type: "spoc", recommendation_id: undefined, shim: SHIM, + fetchTimestamp: FETCH_TIMESTAMP.valueOf(), }, ], window_inner_width: 1000, window_inner_height: 900, + firstVisibleTimestamp: NEWTAB_CREATION_TIMESTAMP.valueOf(), }); await pingSubmitted; @@ -2949,6 +2961,8 @@ add_task( Services.fog.testResetFOG(); const ACTION_POSITION = 42; const SHIM = "Y29uc2lkZXIgeW91ciBjdXJpb3NpdHkgcmV3YXJkZWQ="; + const FETCH_TIMESTAMP = new Date("March 22, 2024 10:15:20"); + const NEWTAB_CREATION_TIMESTAMP = new Date("March 23, 2024 11:10:30"); let action = ac.DiscoveryStreamUserEvent({ event: "CLICK", action_position: ACTION_POSITION, @@ -2957,6 +2971,8 @@ add_task( recommendation_id: undefined, tile_id: 448685088, shim: SHIM, + fetchTimestamp: FETCH_TIMESTAMP.valueOf(), + firstVisibleTimestamp: NEWTAB_CREATION_TIMESTAMP.valueOf(), }, }); @@ -2966,6 +2982,14 @@ add_task( let pingSubmitted = new Promise(resolve => { GleanPings.spoc.testBeforeNextSubmit(reason => { Assert.equal(reason, "click"); + Assert.deepEqual( + Glean.pocket.fetchTimestamp.testGetValue(), + FETCH_TIMESTAMP + ); + Assert.deepEqual( + Glean.pocket.newtabCreationTimestamp.testGetValue(), + NEWTAB_CREATION_TIMESTAMP + ); resolve(); }); }); @@ -3043,6 +3067,8 @@ add_task( Services.fog.testResetFOG(); const ACTION_POSITION = 42; const SHIM = "Y29uc2lkZXIgeW91ciBjdXJpb3NpdHkgcmV3YXJkZWQ="; + const FETCH_TIMESTAMP = new Date("March 22, 2024 10:15:20"); + const NEWTAB_CREATION_TIMESTAMP = new Date("March 23, 2024 11:10:30"); let action = ac.DiscoveryStreamUserEvent({ event: "SAVE_TO_POCKET", action_position: ACTION_POSITION, @@ -3051,6 +3077,8 @@ add_task( recommendation_id: undefined, tile_id: 448685088, shim: SHIM, + fetchTimestamp: FETCH_TIMESTAMP.valueOf(), + newtabCreationTimestamp: NEWTAB_CREATION_TIMESTAMP.valueOf(), }, }); @@ -3064,6 +3092,14 @@ add_task( SHIM, "Pocket shim was recorded" ); + Assert.deepEqual( + Glean.pocket.fetchTimestamp.testGetValue(), + FETCH_TIMESTAMP + ); + Assert.deepEqual( + Glean.pocket.newtabCreationTimestamp.testGetValue(), + NEWTAB_CREATION_TIMESTAMP + ); resolve(); }); diff --git a/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js b/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js index 860e8758a5..4be520fcca 100644 --- a/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js +++ b/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js @@ -8,7 +8,7 @@ const { TopSitesFeed, DEFAULT_TOP_SITES } = ChromeUtils.importESModule( ); const { actionCreators: ac, actionTypes: at } = ChromeUtils.importESModule( - "resource://activity-stream/common/Actions.sys.mjs" + "resource://activity-stream/common/Actions.mjs" ); ChromeUtils.defineESModuleGetters(this, { diff --git a/browser/components/newtab/test/xpcshell/test_WallpaperFeed.js b/browser/components/newtab/test/xpcshell/test_WallpaperFeed.js new file mode 100644 index 0000000000..c6c12c17bf --- /dev/null +++ b/browser/components/newtab/test/xpcshell/test_WallpaperFeed.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { WallpaperFeed } = ChromeUtils.importESModule( + "resource://activity-stream/lib/WallpaperFeed.sys.mjs" +); + +const { actionCreators: ac, actionTypes: at } = ChromeUtils.importESModule( + "resource://activity-stream/common/Actions.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + Utils: "resource://services-settings/Utils.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +const PREF_WALLPAPERS_ENABLED = + "browser.newtabpage.activity-stream.newtabWallpapers.enabled"; + +add_task(async function test_construction() { + let feed = new WallpaperFeed(); + + info("WallpaperFeed constructor should create initial values"); + + Assert.ok(feed, "Could construct a WallpaperFeed"); + Assert.ok(feed.loaded === false, "WallpaperFeed is not loaded"); + Assert.ok( + feed.wallpaperClient === "", + "wallpaperClient is initialized as an empty string" + ); + Assert.ok( + feed.wallpaperDB === "", + "wallpaperDB is initialized as an empty string" + ); + Assert.ok( + feed.baseAttachmentURL === "", + "baseAttachmentURL is initialized as an empty string" + ); +}); + +add_task(async function test_onAction_INIT() { + let sandbox = sinon.createSandbox(); + let feed = new WallpaperFeed(); + Services.prefs.setBoolPref(PREF_WALLPAPERS_ENABLED, true); + const attachment = { + attachment: { + location: "attachment", + }, + }; + sandbox.stub(feed, "RemoteSettings").returns({ + get: () => [attachment], + on: () => {}, + }); + sandbox.stub(Utils, "SERVER_URL").returns("http://localhost:8888/v1"); + feed.store = { + dispatch: sinon.spy(), + }; + sandbox.stub(feed, "fetch").resolves({ + json: () => ({ + capabilities: { + attachments: { + base_url: "http://localhost:8888/base_url/", + }, + }, + }), + }); + + info("WallpaperFeed.onAction INIT should initialize wallpapers"); + + await feed.onAction({ + type: at.INIT, + }); + + Assert.ok(feed.store.dispatch.calledOnce); + Assert.ok( + feed.store.dispatch.calledWith( + ac.BroadcastToContent({ + type: at.WALLPAPERS_SET, + data: [ + { + ...attachment, + wallpaperUrl: "http://localhost:8888/base_url/attachment", + }, + ], + meta: { + isStartup: true, + }, + }) + ) + ); + Services.prefs.clearUserPref(PREF_WALLPAPERS_ENABLED); + sandbox.restore(); +}); + +add_task(async function test_onAction_PREF_CHANGED() { + let sandbox = sinon.createSandbox(); + let feed = new WallpaperFeed(); + Services.prefs.setBoolPref(PREF_WALLPAPERS_ENABLED, true); + sandbox.stub(feed, "wallpaperSetup").returns(); + + info("WallpaperFeed.onAction PREF_CHANGED should call wallpaperSetup"); + + feed.onAction({ + type: at.PREF_CHANGED, + data: { name: "newtabWallpapers.enabled" }, + }); + + Assert.ok(feed.wallpaperSetup.calledOnce); + Assert.ok(feed.wallpaperSetup.calledWith(false)); + + Services.prefs.clearUserPref(PREF_WALLPAPERS_ENABLED); + sandbox.restore(); +}); diff --git a/browser/components/newtab/test/xpcshell/xpcshell.toml b/browser/components/newtab/test/xpcshell/xpcshell.toml index 87d73669d3..13c11b0541 100644 --- a/browser/components/newtab/test/xpcshell/xpcshell.toml +++ b/browser/components/newtab/test/xpcshell/xpcshell.toml @@ -26,3 +26,5 @@ support-files = ["../schemas/*.schema.json"] ["test_TopSitesFeed.js"] ["test_TopSitesFeed_glean.js"] + +["test_WallpaperFeed.js"] diff --git a/browser/components/newtab/webpack.system-addon.config.js b/browser/components/newtab/webpack.system-addon.config.js index a0400ec39e..68a384ea71 100644 --- a/browser/components/newtab/webpack.system-addon.config.js +++ b/browser/components/newtab/webpack.system-addon.config.js @@ -48,7 +48,7 @@ module.exports = (env = {}) => ({ }, // This resolve config allows us to import with paths relative to the root directory, e.g. "lib/ActivityStream.sys.mjs" resolve: { - extensions: [".js", ".jsx"], + extensions: [".js", ".jsx", ".mjs"], modules: ["node_modules", "."], }, externals: { |