From d8bbc7858622b6d9c278469aab701ca0b609cddf Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 15 May 2024 05:35:49 +0200 Subject: Merging upstream version 126.0. Signed-off-by: Daniel Baumann --- .../newtab/content-src/activity-stream.jsx | 5 +- .../newtab/content-src/components/Base/Base.jsx | 150 +++++++++- .../newtab/content-src/components/Base/_Base.scss | 38 ++- .../newtab/content-src/components/Card/Card.jsx | 5 +- .../newtab/content-src/components/Card/types.js | 30 -- .../newtab/content-src/components/Card/types.mjs | 30 ++ .../CollapsibleSection/CollapsibleSection.jsx | 2 +- .../ComponentPerfTimer/ComponentPerfTimer.jsx | 5 +- .../components/ConfirmDialog/ConfirmDialog.jsx | 2 +- .../components/ContextMenu/ContextMenu.jsx | 4 +- .../ContentSection/ContentSection.jsx | 15 +- .../components/CustomizeMenu/CustomizeMenu.jsx | 4 +- .../components/CustomizeMenu/_CustomizeMenu.scss | 4 + .../DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx | 11 +- .../DiscoveryStreamAdmin/SimpleHashRouter.jsx | 8 +- .../DiscoveryStreamBase/DiscoveryStreamBase.jsx | 6 +- .../CardGrid/CardGrid.jsx | 9 +- .../CollectionCardGrid/CollectionCardGrid.jsx | 2 +- .../DiscoveryStreamComponents/DSCard/DSCard.jsx | 11 +- .../DSContextFooter/DSContextFooter.jsx | 2 +- .../DSEmptyState/DSEmptyState.jsx | 5 +- .../DSLinkMenu/DSLinkMenu.jsx | 2 +- .../DSPrivacyModal/DSPrivacyModal.jsx | 5 +- .../DSSignup/DSSignup.jsx | 2 +- .../DSTextPromo/DSTextPromo.jsx | 2 +- .../FeatureHighlight/FeatureHighlight.jsx | 2 +- .../Navigation/Navigation.jsx | 2 +- .../SafeAnchor/SafeAnchor.jsx | 5 +- .../TopicsWidget/TopicsWidget.jsx | 2 +- .../ImpressionStats.jsx | 11 +- .../content-src/components/LinkMenu/LinkMenu.jsx | 2 +- .../components/ModalOverlay/ModalOverlay.jsx | 2 +- .../content-src/components/Search/Search.jsx | 5 +- .../content-src/components/Sections/Sections.jsx | 9 +- .../components/TopSites/SearchShortcutsForm.jsx | 5 +- .../content-src/components/TopSites/TopSite.jsx | 5 +- .../components/TopSites/TopSiteForm.jsx | 5 +- .../TopSites/TopSiteImpressionWrapper.jsx | 6 +- .../content-src/components/TopSites/TopSites.jsx | 7 +- .../components/TopSites/TopSitesConstants.js | 39 --- .../components/TopSites/TopSitesConstants.mjs | 39 +++ .../WallpapersSection/WallpapersSection.jsx | 100 +++++++ .../WallpapersSection/_WallpapersSection.scss | 87 ++++++ .../components/newtab/content-src/lib/constants.js | 38 --- .../newtab/content-src/lib/constants.mjs | 38 +++ .../content-src/lib/detect-user-session-start.js | 82 ------ .../content-src/lib/detect-user-session-start.mjs | 82 ++++++ .../newtab/content-src/lib/init-store.js | 140 ---------- .../newtab/content-src/lib/init-store.mjs | 143 ++++++++++ .../newtab/content-src/lib/link-menu-options.js | 309 --------------------- .../newtab/content-src/lib/link-menu-options.mjs | 309 +++++++++++++++++++++ .../newtab/content-src/lib/perf-service.js | 104 ------- .../newtab/content-src/lib/perf-service.mjs | 102 +++++++ .../newtab/content-src/lib/screenshot-utils.js | 61 ---- .../newtab/content-src/lib/screenshot-utils.mjs | 61 ++++ .../newtab/content-src/lib/selectLayoutRender.js | 255 ----------------- .../newtab/content-src/lib/selectLayoutRender.mjs | 255 +++++++++++++++++ .../content-src/styles/_activity-stream.scss | 12 + 58 files changed, 1519 insertions(+), 1164 deletions(-) delete mode 100644 browser/components/newtab/content-src/components/Card/types.js create mode 100644 browser/components/newtab/content-src/components/Card/types.mjs delete mode 100644 browser/components/newtab/content-src/components/TopSites/TopSitesConstants.js create mode 100644 browser/components/newtab/content-src/components/TopSites/TopSitesConstants.mjs create mode 100644 browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx create mode 100644 browser/components/newtab/content-src/components/WallpapersSection/_WallpapersSection.scss delete mode 100644 browser/components/newtab/content-src/lib/constants.js create mode 100644 browser/components/newtab/content-src/lib/constants.mjs delete mode 100644 browser/components/newtab/content-src/lib/detect-user-session-start.js create mode 100644 browser/components/newtab/content-src/lib/detect-user-session-start.mjs delete mode 100644 browser/components/newtab/content-src/lib/init-store.js create mode 100644 browser/components/newtab/content-src/lib/init-store.mjs delete mode 100644 browser/components/newtab/content-src/lib/link-menu-options.js create mode 100644 browser/components/newtab/content-src/lib/link-menu-options.mjs delete mode 100644 browser/components/newtab/content-src/lib/perf-service.js create mode 100644 browser/components/newtab/content-src/lib/perf-service.mjs delete mode 100644 browser/components/newtab/content-src/lib/screenshot-utils.js create mode 100644 browser/components/newtab/content-src/lib/screenshot-utils.mjs delete mode 100644 browser/components/newtab/content-src/lib/selectLayoutRender.js create mode 100644 browser/components/newtab/content-src/lib/selectLayoutRender.mjs (limited to 'browser/components/newtab/content-src') 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 }) => (
@@ -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.js deleted file mode 100644 index 0b17eea408..0000000000 --- a/browser/components/newtab/content-src/components/Card/types.js +++ /dev/null @@ -1,30 +0,0 @@ -/* 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/. */ - -export const cardContextTypes = { - history: { - fluentID: "newtab-label-visited", - icon: "history-item", - }, - removedBookmark: { - fluentID: "newtab-label-removed-bookmark", - icon: "bookmark-removed", - }, - bookmark: { - fluentID: "newtab-label-bookmarked", - icon: "bookmark-added", - }, - trending: { - fluentID: "newtab-label-recommended", - icon: "trending", - }, - pocket: { - fluentID: "newtab-label-saved", - icon: "pocket", - }, - download: { - fluentID: "newtab-label-download", - icon: "download", - }, -}; diff --git a/browser/components/newtab/content-src/components/Card/types.mjs b/browser/components/newtab/content-src/components/Card/types.mjs new file mode 100644 index 0000000000..0b17eea408 --- /dev/null +++ b/browser/components/newtab/content-src/components/Card/types.mjs @@ -0,0 +1,30 @@ +/* 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/. */ + +export const cardContextTypes = { + history: { + fluentID: "newtab-label-visited", + icon: "history-item", + }, + removedBookmark: { + fluentID: "newtab-label-removed-bookmark", + icon: "bookmark-removed", + }, + bookmark: { + fluentID: "newtab-label-bookmarked", + icon: "bookmark-added", + }, + trending: { + fluentID: "newtab-label-recommended", + icon: "trending", + }, + pocket: { + fluentID: "newtab-label-saved", + icon: "pocket", + }, + download: { + fluentID: "newtab-label-download", + icon: "download", + }, +}; 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 (
+ {wallpapersEnabled && ( +
+

+ +
+ )}
(this.closeButton = c)} /> - ); - case "CollectionCardGrid": + case "CollectionCardGrid": { const { DiscoveryStream } = this.props; return ( ); + } case "CardGrid": return ( ); 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} /> {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.js deleted file mode 100644 index f488896238..0000000000 --- a/browser/components/newtab/content-src/components/TopSites/TopSitesConstants.js +++ /dev/null @@ -1,39 +0,0 @@ -/* 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/. */ - -export const TOP_SITES_SOURCE = "TOP_SITES"; -export const TOP_SITES_CONTEXT_MENU_OPTIONS = [ - "CheckPinTopSite", - "EditTopSite", - "Separator", - "OpenInNewWindow", - "OpenInPrivateWindow", - "Separator", - "BlockUrl", - "DeleteUrl", -]; -export const TOP_SITES_SPOC_CONTEXT_MENU_OPTIONS = [ - "OpenInNewWindow", - "OpenInPrivateWindow", - "Separator", - "BlockUrl", - "ShowPrivacyInfo", -]; -export 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 -export const TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS = [ - "CheckPinTopSite", - "Separator", - "BlockUrl", -]; -// minimum size necessary to show a rich icon instead of a screenshot -export const MIN_RICH_FAVICON_SIZE = 96; -// minimum size necessary to show any icon -export const MIN_SMALL_FAVICON_SIZE = 16; diff --git a/browser/components/newtab/content-src/components/TopSites/TopSitesConstants.mjs b/browser/components/newtab/content-src/components/TopSites/TopSitesConstants.mjs new file mode 100644 index 0000000000..f488896238 --- /dev/null +++ b/browser/components/newtab/content-src/components/TopSites/TopSitesConstants.mjs @@ -0,0 +1,39 @@ +/* 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/. */ + +export const TOP_SITES_SOURCE = "TOP_SITES"; +export const TOP_SITES_CONTEXT_MENU_OPTIONS = [ + "CheckPinTopSite", + "EditTopSite", + "Separator", + "OpenInNewWindow", + "OpenInPrivateWindow", + "Separator", + "BlockUrl", + "DeleteUrl", +]; +export const TOP_SITES_SPOC_CONTEXT_MENU_OPTIONS = [ + "OpenInNewWindow", + "OpenInPrivateWindow", + "Separator", + "BlockUrl", + "ShowPrivacyInfo", +]; +export 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 +export const TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS = [ + "CheckPinTopSite", + "Separator", + "BlockUrl", +]; +// minimum size necessary to show a rich icon instead of a screenshot +export const MIN_RICH_FAVICON_SIZE = 96; +// minimum size necessary to show any icon +export const MIN_SMALL_FAVICON_SIZE = 16; 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 ( +
+
+ {wallpaperList.map(({ title, theme, fluent_id }) => { + return ( + <> + + + + ); + })} +
+
+ ); + } +} + +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.js deleted file mode 100644 index 2c96160b4b..0000000000 --- a/browser/components/newtab/content-src/lib/constants.js +++ /dev/null @@ -1,38 +0,0 @@ -/* 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/. */ - -export const IS_NEWTAB = - global.document && global.document.documentURI === "about:newtab"; -export const NEWTAB_DARK_THEME = { - ntp_background: { - r: 42, - g: 42, - b: 46, - a: 1, - }, - ntp_card_background: { - r: 66, - g: 65, - b: 77, - a: 1, - }, - ntp_text: { - r: 249, - g: 249, - b: 250, - a: 1, - }, - sidebar: { - r: 56, - g: 56, - b: 61, - a: 1, - }, - sidebar_text: { - r: 249, - g: 249, - b: 250, - a: 1, - }, -}; diff --git a/browser/components/newtab/content-src/lib/constants.mjs b/browser/components/newtab/content-src/lib/constants.mjs new file mode 100644 index 0000000000..4f07a77e29 --- /dev/null +++ b/browser/components/newtab/content-src/lib/constants.mjs @@ -0,0 +1,38 @@ +/* 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/. */ + +export const IS_NEWTAB = + globalThis.document && globalThis.document.documentURI === "about:newtab"; +export const NEWTAB_DARK_THEME = { + ntp_background: { + r: 42, + g: 42, + b: 46, + a: 1, + }, + ntp_card_background: { + r: 66, + g: 65, + b: 77, + a: 1, + }, + ntp_text: { + r: 249, + g: 249, + b: 250, + a: 1, + }, + sidebar: { + r: 56, + g: 56, + b: 61, + a: 1, + }, + sidebar_text: { + r: 249, + g: 249, + b: 250, + a: 1, + }, +}; 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.js deleted file mode 100644 index 43aa388967..0000000000 --- a/browser/components/newtab/content-src/lib/detect-user-session-start.js +++ /dev/null @@ -1,82 +0,0 @@ -/* 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; -import { perfService as perfSvc } from "content-src/lib/perf-service"; - -const VISIBLE = "visible"; -const VISIBILITY_CHANGE_EVENT = "visibilitychange"; - -export class DetectUserSessionStart { - constructor(store, options = {}) { - this._store = store; - // Overrides for testing - this.document = options.document || global.document; - this._perfService = options.perfService || perfSvc; - this._onVisibilityChange = this._onVisibilityChange.bind(this); - } - - /** - * sendEventOrAddListener - Notify immediately if the page is already visible, - * or else set up a listener for when visibility changes. - * This is needed for accurate session tracking for telemetry, - * because tabs are pre-loaded. - */ - sendEventOrAddListener() { - if (this.document.visibilityState === VISIBLE) { - // If the document is already visible, to the user, send a notification - // immediately that a session has started. - this._sendEvent(); - } else { - // If the document is not visible, listen for when it does become visible. - this.document.addEventListener( - VISIBILITY_CHANGE_EVENT, - this._onVisibilityChange - ); - } - } - - /** - * _sendEvent - Sends a message to the main process to indicate the current - * tab is now visible to the user, includes the - * visibility_event_rcvd_ts time in ms from the UNIX epoch. - */ - _sendEvent() { - this._perfService.mark("visibility_event_rcvd_ts"); - - try { - let visibility_event_rcvd_ts = - this._perfService.getMostRecentAbsMarkStartByName( - "visibility_event_rcvd_ts" - ); - - this._store.dispatch( - ac.AlsoToMain({ - type: at.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. - } - } - - /** - * _onVisibilityChange - If the visibility has changed to visible, sends a notification - * and removes the event listener. This should only be called once per tab. - */ - _onVisibilityChange() { - if (this.document.visibilityState === VISIBLE) { - this._sendEvent(); - this.document.removeEventListener( - VISIBILITY_CHANGE_EVENT, - this._onVisibilityChange - ); - } - } -} diff --git a/browser/components/newtab/content-src/lib/detect-user-session-start.mjs b/browser/components/newtab/content-src/lib/detect-user-session-start.mjs new file mode 100644 index 0000000000..d4c36efd4a --- /dev/null +++ b/browser/components/newtab/content-src/lib/detect-user-session-start.mjs @@ -0,0 +1,82 @@ +/* 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 { + actionCreators as ac, + actionTypes as at, +} from "../../common/Actions.mjs"; +import { perfService as perfSvc } from "./perf-service.mjs"; + +const VISIBLE = "visible"; +const VISIBILITY_CHANGE_EVENT = "visibilitychange"; + +export class DetectUserSessionStart { + constructor(store, options = {}) { + this._store = store; + // Overrides for testing + this.document = options.document || globalThis.document; + this._perfService = options.perfService || perfSvc; + this._onVisibilityChange = this._onVisibilityChange.bind(this); + } + + /** + * sendEventOrAddListener - Notify immediately if the page is already visible, + * or else set up a listener for when visibility changes. + * This is needed for accurate session tracking for telemetry, + * because tabs are pre-loaded. + */ + sendEventOrAddListener() { + if (this.document.visibilityState === VISIBLE) { + // If the document is already visible, to the user, send a notification + // immediately that a session has started. + this._sendEvent(); + } else { + // If the document is not visible, listen for when it does become visible. + this.document.addEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + } + + /** + * _sendEvent - Sends a message to the main process to indicate the current + * tab is now visible to the user, includes the + * visibility_event_rcvd_ts time in ms from the UNIX epoch. + */ + _sendEvent() { + this._perfService.mark("visibility_event_rcvd_ts"); + + try { + let visibility_event_rcvd_ts = + this._perfService.getMostRecentAbsMarkStartByName( + "visibility_event_rcvd_ts" + ); + + this._store.dispatch( + ac.AlsoToMain({ + type: at.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. + } + } + + /** + * _onVisibilityChange - If the visibility has changed to visible, sends a notification + * and removes the event listener. This should only be called once per tab. + */ + _onVisibilityChange() { + if (this.document.visibilityState === VISIBLE) { + this._sendEvent(); + this.document.removeEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + } +} diff --git a/browser/components/newtab/content-src/lib/init-store.js b/browser/components/newtab/content-src/lib/init-store.js deleted file mode 100644 index f0ab2db86a..0000000000 --- a/browser/components/newtab/content-src/lib/init-store.js +++ /dev/null @@ -1,140 +0,0 @@ -/* 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/. */ - -/* eslint-env mozilla/remote-page */ - -import { - actionCreators as ac, - actionTypes as at, - actionUtils as au, -} from "common/Actions.sys.mjs"; -import { applyMiddleware, combineReducers, createStore } from "redux"; - -export const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE"; -export const OUTGOING_MESSAGE_NAME = "ActivityStream:ContentToMain"; -export const INCOMING_MESSAGE_NAME = "ActivityStream:MainToContent"; - -/** - * A higher-order function which returns a reducer that, on MERGE_STORE action, - * will return the action.data object merged into the previous state. - * - * For all other actions, it merely calls mainReducer. - * - * Because we want this to merge the entire state object, it's written as a - * higher order function which takes the main reducer (itself often a call to - * combineReducers) as a parameter. - * - * @param {function} mainReducer reducer to call if action != MERGE_STORE_ACTION - * @return {function} a reducer that, on MERGE_STORE_ACTION action, - * will return the action.data object merged - * into the previous state, and the result - * of calling mainReducer otherwise. - */ -function mergeStateReducer(mainReducer) { - return (prevState, action) => { - if (action.type === MERGE_STORE_ACTION) { - return { ...prevState, ...action.data }; - } - - return mainReducer(prevState, action); - }; -} - -/** - * messageMiddleware - Middleware that looks for SentToMain type actions, and sends them if necessary - */ -const messageMiddleware = () => next => action => { - const skipLocal = action.meta && action.meta.skipLocal; - if (au.isSendToMain(action)) { - RPMSendAsyncMessage(OUTGOING_MESSAGE_NAME, action); - } - if (!skipLocal) { - next(action); - } -}; - -export 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; - getState.didRequestInitialState = false; - return next => action => { - 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 - ) { - return null; - } - return next(action); - } - - const isMergeStoreAction = action.type === MERGE_STORE_ACTION; - const isRehydrationRequest = action.type === at.NEW_TAB_STATE_REQUEST; - - if (isRehydrationRequest) { - getState.didRequestInitialState = true; - return next(action); - } - - if (isMergeStoreAction) { - getState.didRehydrate = true; - return next(action); - } - - // If init happened after our request was made, we need to re-request - if (getState.didRequestInitialState && action.type === at.INIT) { - return next(ac.AlsoToMain({ type: at.NEW_TAB_STATE_REQUEST })); - } - - if ( - au.isBroadcastToContent(action) || - au.isSendToOneContent(action) || - au.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); - }; -}; - -/** - * initStore - Create a store and listen for incoming actions - * - * @param {object} reducers An object containing Redux reducers - * @param {object} intialState (optional) The initial state of the store, if desired - * @return {object} A redux store - */ -export function initStore(reducers, initialState) { - const store = createStore( - mergeStateReducer(combineReducers(reducers)), - initialState, - global.RPMAddMessageListener && - applyMiddleware(rehydrationMiddleware, messageMiddleware) - ); - - if (global.RPMAddMessageListener) { - global.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 - }` - ); - } - }); - } - - return store; -} diff --git a/browser/components/newtab/content-src/lib/init-store.mjs b/browser/components/newtab/content-src/lib/init-store.mjs new file mode 100644 index 0000000000..85b3b0b470 --- /dev/null +++ b/browser/components/newtab/content-src/lib/init-store.mjs @@ -0,0 +1,143 @@ +/* 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/. */ + +/* eslint-env mozilla/remote-page */ + +import { + actionCreators as ac, + actionTypes as at, + actionUtils as au, +} 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"; +export const OUTGOING_MESSAGE_NAME = "ActivityStream:ContentToMain"; +export const INCOMING_MESSAGE_NAME = "ActivityStream:MainToContent"; + +/** + * A higher-order function which returns a reducer that, on MERGE_STORE action, + * will return the action.data object merged into the previous state. + * + * For all other actions, it merely calls mainReducer. + * + * Because we want this to merge the entire state object, it's written as a + * higher order function which takes the main reducer (itself often a call to + * combineReducers) as a parameter. + * + * @param {function} mainReducer reducer to call if action != MERGE_STORE_ACTION + * @return {function} a reducer that, on MERGE_STORE_ACTION action, + * will return the action.data object merged + * into the previous state, and the result + * of calling mainReducer otherwise. + */ +function mergeStateReducer(mainReducer) { + return (prevState, action) => { + if (action.type === MERGE_STORE_ACTION) { + return { ...prevState, ...action.data }; + } + + return mainReducer(prevState, action); + }; +} + +/** + * messageMiddleware - Middleware that looks for SentToMain type actions, and sends them if necessary + */ +const messageMiddleware = () => next => action => { + const skipLocal = action.meta && action.meta.skipLocal; + if (au.isSendToMain(action)) { + RPMSendAsyncMessage(OUTGOING_MESSAGE_NAME, action); + } + if (!skipLocal) { + next(action); + } +}; + +export 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; + getState.didRequestInitialState = false; + return next => action => { + 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 + ) { + return null; + } + return next(action); + } + + const isMergeStoreAction = action.type === MERGE_STORE_ACTION; + const isRehydrationRequest = action.type === at.NEW_TAB_STATE_REQUEST; + + if (isRehydrationRequest) { + getState.didRequestInitialState = true; + return next(action); + } + + if (isMergeStoreAction) { + getState.didRehydrate = true; + return next(action); + } + + // If init happened after our request was made, we need to re-request + if (getState.didRequestInitialState && action.type === at.INIT) { + return next(ac.AlsoToMain({ type: at.NEW_TAB_STATE_REQUEST })); + } + + if ( + au.isBroadcastToContent(action) || + au.isSendToOneContent(action) || + au.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); + }; +}; + +/** + * initStore - Create a store and listen for incoming actions + * + * @param {object} reducers An object containing Redux reducers + * @param {object} intialState (optional) The initial state of the store, if desired + * @return {object} A redux store + */ +export function initStore(reducers, initialState) { + const store = createStore( + mergeStateReducer(combineReducers(reducers)), + initialState, + globalThis.RPMAddMessageListener && + 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 + }` + ); + } + }); + } + + return store; +} diff --git a/browser/components/newtab/content-src/lib/link-menu-options.js b/browser/components/newtab/content-src/lib/link-menu-options.js deleted file mode 100644 index 12e47259c1..0000000000 --- a/browser/components/newtab/content-src/lib/link-menu-options.js +++ /dev/null @@ -1,309 +0,0 @@ -/* 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; - -const _OpenInPrivateWindow = site => ({ - id: "newtab-menu-open-new-private-window", - icon: "new-window-private", - action: ac.OnlyToMain({ - type: at.OPEN_PRIVATE_WINDOW, - data: { url: site.url, referrer: site.referrer }, - }), - userEvent: "OPEN_PRIVATE_WINDOW", -}); - -/** - * List of functions that return items that can be included as menu options in a - * LinkMenu. All functions take the site as the first parameter, and optionally - * the index of the site. - */ -export const LinkMenuOptions = { - Separator: () => ({ type: "separator" }), - EmptyItem: () => ({ type: "empty" }), - ShowPrivacyInfo: () => ({ - id: "newtab-menu-show-privacy-info", - icon: "info", - action: { - type: at.SHOW_PRIVACY_INFO, - }, - userEvent: "SHOW_PRIVACY_INFO", - }), - AboutSponsored: site => ({ - id: "newtab-menu-show-privacy-info", - icon: "info", - action: ac.AlsoToMain({ - type: at.ABOUT_SPONSORED_TOP_SITES, - data: { - advertiser_name: (site.label || site.hostname).toLocaleLowerCase(), - position: site.sponsored_position, - tile_id: site.sponsored_tile_id, - }, - }), - userEvent: "TOPSITE_SPONSOR_INFO", - }), - RemoveBookmark: site => ({ - id: "newtab-menu-remove-bookmark", - icon: "bookmark-added", - action: ac.AlsoToMain({ - type: at.DELETE_BOOKMARK_BY_ID, - data: site.bookmarkGuid, - }), - userEvent: "BOOKMARK_DELETE", - }), - AddBookmark: site => ({ - id: "newtab-menu-bookmark", - icon: "bookmark-hollow", - action: ac.AlsoToMain({ - type: at.BOOKMARK_URL, - data: { url: site.url, title: site.title, type: site.type }, - }), - userEvent: "BOOKMARK_ADD", - }), - OpenInNewWindow: site => ({ - id: "newtab-menu-open-new-window", - icon: "new-window", - action: ac.AlsoToMain({ - type: at.OPEN_NEW_WINDOW, - data: { - referrer: site.referrer, - typedBonus: site.typedBonus, - url: site.url, - sponsored_tile_id: site.sponsored_tile_id, - }, - }), - userEvent: "OPEN_NEW_WINDOW", - }), - // This blocks the url for regular stories, - // but also sends a message to DiscoveryStream with flight_id. - // If DiscoveryStream sees this message for a flight_id - // it also blocks it on the flight_id. - BlockUrl: (site, index, eventSource) => { - return LinkMenuOptions.BlockUrls([site], index, eventSource); - }, - // Same as BlockUrl, cept can work on an array of sites. - BlockUrls: (tiles, pos, eventSource) => ({ - id: "newtab-menu-dismiss", - icon: "dismiss", - action: ac.AlsoToMain({ - type: at.BLOCK_URL, - data: tiles.map(site => ({ - url: site.original_url || site.open_url || site.url, - // pocket_id is only for pocket stories being in highlights, and then dismissed. - 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 } : {}), - // 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(), - } - : {}), - position: pos, - ...(site.sponsored_tile_id ? { tile_id: site.sponsored_tile_id } : {}), - is_pocket_card: site.type === "CardGrid", - })), - }), - impression: ac.ImpressionStats({ - source: eventSource, - block: 0, - tiles: tiles.map((site, index) => ({ - id: site.guid, - pos: pos + index, - ...(site.shim && site.shim.delete ? { shim: site.shim.delete } : {}), - })), - }), - 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) => ({ - id: "menu_action_webext_dismiss", - string_id: "newtab-menu-dismiss", - icon: "dismiss", - action: ac.WebExtEvent(at.WEBEXT_DISMISS, { - source: eventSource, - url: site.url, - action_position: index, - }), - }), - DeleteUrl: (site, index, eventSource, isEnabled, siteInfo) => ({ - id: "newtab-menu-delete-history", - icon: "delete", - action: { - type: at.DIALOG_OPEN, - data: { - onConfirm: [ - ac.AlsoToMain({ - type: at.DELETE_HISTORY_URL, - data: { - url: site.url, - pocket_id: site.pocket_id, - forceBlock: site.bookmarkGuid, - }, - }), - ac.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", - ], - confirm_button_string_id: "newtab-topsites-delete-history-button", - cancel_button_string_id: "newtab-topsites-cancel-button", - icon: "modal-delete", - }, - }, - userEvent: "DIALOG_OPEN", - }), - ShowFile: site => ({ - id: "newtab-menu-show-file", - icon: "search", - action: ac.OnlyToMain({ - type: at.SHOW_DOWNLOAD_FILE, - data: { url: site.url }, - }), - }), - OpenFile: site => ({ - id: "newtab-menu-open-file", - icon: "open-file", - action: ac.OnlyToMain({ - type: at.OPEN_DOWNLOAD_FILE, - data: { url: site.url }, - }), - }), - CopyDownloadLink: site => ({ - id: "newtab-menu-copy-download-link", - icon: "copy", - action: ac.OnlyToMain({ - type: at.COPY_DOWNLOAD_LINK, - data: { url: site.url }, - }), - }), - GoToDownloadPage: site => ({ - id: "newtab-menu-go-to-download-page", - icon: "download", - action: ac.OnlyToMain({ - type: at.OPEN_LINK, - data: { url: site.referrer }, - }), - disabled: !site.referrer, - }), - RemoveDownload: site => ({ - id: "newtab-menu-remove-download", - icon: "delete", - action: ac.OnlyToMain({ - type: at.REMOVE_DOWNLOAD_FILE, - data: { url: site.url }, - }), - }), - PinTopSite: (site, index) => ({ - id: "newtab-menu-pin", - icon: "pin", - action: ac.AlsoToMain({ - type: at.TOP_SITES_PIN, - data: { - site, - index, - }, - }), - userEvent: "PIN", - }), - UnpinTopSite: site => ({ - id: "newtab-menu-unpin", - icon: "unpin", - action: ac.AlsoToMain({ - type: at.TOP_SITES_UNPIN, - data: { site: { url: site.url } }, - }), - userEvent: "UNPIN", - }), - SaveToPocket: (site, index, eventSource = "CARDGRID") => ({ - id: "newtab-menu-save-to-pocket", - icon: "pocket-save", - action: ac.AlsoToMain({ - type: at.SAVE_TO_POCKET, - data: { - site: { url: site.url, title: site.title }, - }, - }), - impression: ac.ImpressionStats({ - source: eventSource, - pocket: 0, - tiles: [ - { - id: site.guid, - pos: index, - ...(site.shim && site.shim.save ? { shim: site.shim.save } : {}), - }, - ], - }), - userEvent: "SAVE_TO_POCKET", - }), - DeleteFromPocket: site => ({ - id: "newtab-menu-delete-pocket", - icon: "pocket-delete", - action: ac.AlsoToMain({ - type: at.DELETE_FROM_POCKET, - data: { pocket_id: site.pocket_id }, - }), - userEvent: "DELETE_FROM_POCKET", - }), - ArchiveFromPocket: site => ({ - id: "newtab-menu-archive-pocket", - icon: "pocket-archive", - action: ac.AlsoToMain({ - type: at.ARCHIVE_FROM_POCKET, - data: { pocket_id: site.pocket_id }, - }), - userEvent: "ARCHIVE_FROM_POCKET", - }), - EditTopSite: (site, index) => ({ - id: "newtab-menu-edit-topsites", - icon: "edit", - action: { - type: at.TOP_SITES_EDIT, - 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(), -}; diff --git a/browser/components/newtab/content-src/lib/link-menu-options.mjs b/browser/components/newtab/content-src/lib/link-menu-options.mjs new file mode 100644 index 0000000000..f10a5e34c6 --- /dev/null +++ b/browser/components/newtab/content-src/lib/link-menu-options.mjs @@ -0,0 +1,309 @@ +/* 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 { + actionCreators as ac, + actionTypes as at, +} from "../../common/Actions.mjs"; + +const _OpenInPrivateWindow = site => ({ + id: "newtab-menu-open-new-private-window", + icon: "new-window-private", + action: ac.OnlyToMain({ + type: at.OPEN_PRIVATE_WINDOW, + data: { url: site.url, referrer: site.referrer }, + }), + userEvent: "OPEN_PRIVATE_WINDOW", +}); + +/** + * List of functions that return items that can be included as menu options in a + * LinkMenu. All functions take the site as the first parameter, and optionally + * the index of the site. + */ +export const LinkMenuOptions = { + Separator: () => ({ type: "separator" }), + EmptyItem: () => ({ type: "empty" }), + ShowPrivacyInfo: () => ({ + id: "newtab-menu-show-privacy-info", + icon: "info", + action: { + type: at.SHOW_PRIVACY_INFO, + }, + userEvent: "SHOW_PRIVACY_INFO", + }), + AboutSponsored: site => ({ + id: "newtab-menu-show-privacy-info", + icon: "info", + action: ac.AlsoToMain({ + type: at.ABOUT_SPONSORED_TOP_SITES, + data: { + advertiser_name: (site.label || site.hostname).toLocaleLowerCase(), + position: site.sponsored_position, + tile_id: site.sponsored_tile_id, + }, + }), + userEvent: "TOPSITE_SPONSOR_INFO", + }), + RemoveBookmark: site => ({ + id: "newtab-menu-remove-bookmark", + icon: "bookmark-added", + action: ac.AlsoToMain({ + type: at.DELETE_BOOKMARK_BY_ID, + data: site.bookmarkGuid, + }), + userEvent: "BOOKMARK_DELETE", + }), + AddBookmark: site => ({ + id: "newtab-menu-bookmark", + icon: "bookmark-hollow", + action: ac.AlsoToMain({ + type: at.BOOKMARK_URL, + data: { url: site.url, title: site.title, type: site.type }, + }), + userEvent: "BOOKMARK_ADD", + }), + OpenInNewWindow: site => ({ + id: "newtab-menu-open-new-window", + icon: "new-window", + action: ac.AlsoToMain({ + type: at.OPEN_NEW_WINDOW, + data: { + referrer: site.referrer, + typedBonus: site.typedBonus, + url: site.url, + sponsored_tile_id: site.sponsored_tile_id, + }, + }), + userEvent: "OPEN_NEW_WINDOW", + }), + // This blocks the url for regular stories, + // but also sends a message to DiscoveryStream with flight_id. + // If DiscoveryStream sees this message for a flight_id + // it also blocks it on the flight_id. + BlockUrl: (site, index, eventSource) => { + return LinkMenuOptions.BlockUrls([site], index, eventSource); + }, + // Same as BlockUrl, cept can work on an array of sites. + BlockUrls: (tiles, pos, eventSource) => ({ + id: "newtab-menu-dismiss", + icon: "dismiss", + action: ac.AlsoToMain({ + type: at.BLOCK_URL, + data: tiles.map(site => ({ + url: site.original_url || site.open_url || site.url, + // pocket_id is only for pocket stories being in highlights, and then dismissed. + 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 } : {}), + // 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(), + } + : {}), + position: pos, + ...(site.sponsored_tile_id ? { tile_id: site.sponsored_tile_id } : {}), + is_pocket_card: site.type === "CardGrid", + })), + }), + impression: ac.ImpressionStats({ + source: eventSource, + block: 0, + tiles: tiles.map((site, index) => ({ + id: site.guid, + pos: pos + index, + ...(site.shim && site.shim.delete ? { shim: site.shim.delete } : {}), + })), + }), + 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) => ({ + id: "menu_action_webext_dismiss", + string_id: "newtab-menu-dismiss", + icon: "dismiss", + action: ac.WebExtEvent(at.WEBEXT_DISMISS, { + source: eventSource, + url: site.url, + action_position: index, + }), + }), + DeleteUrl: (site, index, eventSource, isEnabled, siteInfo) => ({ + id: "newtab-menu-delete-history", + icon: "delete", + action: { + type: at.DIALOG_OPEN, + data: { + onConfirm: [ + ac.AlsoToMain({ + type: at.DELETE_HISTORY_URL, + data: { + url: site.url, + pocket_id: site.pocket_id, + forceBlock: site.bookmarkGuid, + }, + }), + ac.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", + ], + confirm_button_string_id: "newtab-topsites-delete-history-button", + cancel_button_string_id: "newtab-topsites-cancel-button", + icon: "modal-delete", + }, + }, + userEvent: "DIALOG_OPEN", + }), + ShowFile: site => ({ + id: "newtab-menu-show-file", + icon: "search", + action: ac.OnlyToMain({ + type: at.SHOW_DOWNLOAD_FILE, + data: { url: site.url }, + }), + }), + OpenFile: site => ({ + id: "newtab-menu-open-file", + icon: "open-file", + action: ac.OnlyToMain({ + type: at.OPEN_DOWNLOAD_FILE, + data: { url: site.url }, + }), + }), + CopyDownloadLink: site => ({ + id: "newtab-menu-copy-download-link", + icon: "copy", + action: ac.OnlyToMain({ + type: at.COPY_DOWNLOAD_LINK, + data: { url: site.url }, + }), + }), + GoToDownloadPage: site => ({ + id: "newtab-menu-go-to-download-page", + icon: "download", + action: ac.OnlyToMain({ + type: at.OPEN_LINK, + data: { url: site.referrer }, + }), + disabled: !site.referrer, + }), + RemoveDownload: site => ({ + id: "newtab-menu-remove-download", + icon: "delete", + action: ac.OnlyToMain({ + type: at.REMOVE_DOWNLOAD_FILE, + data: { url: site.url }, + }), + }), + PinTopSite: (site, index) => ({ + id: "newtab-menu-pin", + icon: "pin", + action: ac.AlsoToMain({ + type: at.TOP_SITES_PIN, + data: { + site, + index, + }, + }), + userEvent: "PIN", + }), + UnpinTopSite: site => ({ + id: "newtab-menu-unpin", + icon: "unpin", + action: ac.AlsoToMain({ + type: at.TOP_SITES_UNPIN, + data: { site: { url: site.url } }, + }), + userEvent: "UNPIN", + }), + SaveToPocket: (site, index, eventSource = "CARDGRID") => ({ + id: "newtab-menu-save-to-pocket", + icon: "pocket-save", + action: ac.AlsoToMain({ + type: at.SAVE_TO_POCKET, + data: { + site: { url: site.url, title: site.title }, + }, + }), + impression: ac.ImpressionStats({ + source: eventSource, + pocket: 0, + tiles: [ + { + id: site.guid, + pos: index, + ...(site.shim && site.shim.save ? { shim: site.shim.save } : {}), + }, + ], + }), + userEvent: "SAVE_TO_POCKET", + }), + DeleteFromPocket: site => ({ + id: "newtab-menu-delete-pocket", + icon: "pocket-delete", + action: ac.AlsoToMain({ + type: at.DELETE_FROM_POCKET, + data: { pocket_id: site.pocket_id }, + }), + userEvent: "DELETE_FROM_POCKET", + }), + ArchiveFromPocket: site => ({ + id: "newtab-menu-archive-pocket", + icon: "pocket-archive", + action: ac.AlsoToMain({ + type: at.ARCHIVE_FROM_POCKET, + data: { pocket_id: site.pocket_id }, + }), + userEvent: "ARCHIVE_FROM_POCKET", + }), + EditTopSite: (site, index) => ({ + id: "newtab-menu-edit-topsites", + icon: "edit", + action: { + type: at.TOP_SITES_EDIT, + 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(), +}; diff --git a/browser/components/newtab/content-src/lib/perf-service.js b/browser/components/newtab/content-src/lib/perf-service.js deleted file mode 100644 index 6ea99ce877..0000000000 --- a/browser/components/newtab/content-src/lib/perf-service.js +++ /dev/null @@ -1,104 +0,0 @@ -/* 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/. */ - -"use strict"; - -let usablePerfObj = window.performance; - -export function _PerfService(options) { - // For testing, so that we can use a fake Window.performance object with - // known state. - if (options && options.performanceObj) { - this._perf = options.performanceObj; - } else { - this._perf = usablePerfObj; - } -} - -_PerfService.prototype = { - /** - * Calls the underlying mark() method on the appropriate Window.performance - * object to add a mark with the given name to the appropriate performance - * timeline. - * - * @param {String} name the name to give the current mark - * @return {void} - */ - mark: function mark(str) { - this._perf.mark(str); - }, - - /** - * Calls the underlying getEntriesByName on the appropriate Window.performance - * object. - * - * @param {String} name - * @param {String} type eg "mark" - * @return {Array} Performance* objects - */ - getEntriesByName: function getEntriesByName(name, type) { - return this._perf.getEntriesByName(name, type); - }, - - /** - * The timeOrigin property from the appropriate performance object. - * Used to ensure that timestamps from the add-on code and the content code - * are comparable. - * - * @note If this is called from a context without a window - * (eg a JSM in chrome), it will return the timeOrigin of the XUL hidden - * window, which appears to be the first created window (and thus - * timeOrigin) in the browser. Note also, however, there is also a private - * hidden window, presumably for private browsing, which appears to be - * created dynamically later. Exactly how/when that shows up needs to be - * investigated. - * - * @return {Number} A double of milliseconds with a precision of 0.5us. - */ - 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) - * be comparable across both chrome and content. - * - * @return {Number} - */ - absNow: function absNow() { - return this.timeOrigin + this._perf.now(); - }, - - /** - * This returns the absolute startTime from the most recent performance.mark() - * with the given name. - * - * @param {String} name the name to lookup the start time for - * - * @return {Number} the returned start time, as a DOMHighResTimeStamp - * - * @throws {Error} "No Marks with the name ..." if none are available - * - * @note Always surround calls to this by try/catch. Otherwise your code - * may fail when the `privacy.resistFingerprinting` pref is true. When - * this pref is set, all attempts to get marks will likely fail, which will - * cause this method to throw. - * - * See [bug 1369303](https://bugzilla.mozilla.org/show_bug.cgi?id=1369303) - * for more info. - */ - getMostRecentAbsMarkStartByName(name) { - let entries = this.getEntriesByName(name, "mark"); - - if (!entries.length) { - throw new Error(`No marks with the name ${name}`); - } - - let mostRecentEntry = entries[entries.length - 1]; - return this._perf.timeOrigin + mostRecentEntry.startTime; - }, -}; - -export const perfService = new _PerfService(); diff --git a/browser/components/newtab/content-src/lib/perf-service.mjs b/browser/components/newtab/content-src/lib/perf-service.mjs new file mode 100644 index 0000000000..25fc430726 --- /dev/null +++ b/browser/components/newtab/content-src/lib/perf-service.mjs @@ -0,0 +1,102 @@ +/* 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; + +export function _PerfService(options) { + // For testing, so that we can use a fake Window.performance object with + // known state. + if (options && options.performanceObj) { + this._perf = options.performanceObj; + } else { + this._perf = usablePerfObj; + } +} + +_PerfService.prototype = { + /** + * Calls the underlying mark() method on the appropriate Window.performance + * object to add a mark with the given name to the appropriate performance + * timeline. + * + * @param {String} name the name to give the current mark + * @return {void} + */ + mark: function mark(str) { + this._perf.mark(str); + }, + + /** + * Calls the underlying getEntriesByName on the appropriate Window.performance + * object. + * + * @param {String} name + * @param {String} type eg "mark" + * @return {Array} Performance* objects + */ + 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 + * are comparable. + * + * @note If this is called from a context without a window + * (eg a JSM in chrome), it will return the timeOrigin of the XUL hidden + * window, which appears to be the first created window (and thus + * timeOrigin) in the browser. Note also, however, there is also a private + * hidden window, presumably for private browsing, which appears to be + * created dynamically later. Exactly how/when that shows up needs to be + * investigated. + * + * @return {Number} A double of milliseconds with a precision of 0.5us. + */ + 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) + * be comparable across both chrome and content. + * + * @return {Number} + */ + absNow: function absNow() { + return this.timeOrigin + this._perf.now(); + }, + + /** + * This returns the absolute startTime from the most recent performance.mark() + * with the given name. + * + * @param {String} name the name to lookup the start time for + * + * @return {Number} the returned start time, as a DOMHighResTimeStamp + * + * @throws {Error} "No Marks with the name ..." if none are available + * + * @note Always surround calls to this by try/catch. Otherwise your code + * may fail when the `privacy.resistFingerprinting` pref is true. When + * this pref is set, all attempts to get marks will likely fail, which will + * cause this method to throw. + * + * See [bug 1369303](https://bugzilla.mozilla.org/show_bug.cgi?id=1369303) + * for more info. + */ + getMostRecentAbsMarkStartByName(entryName) { + let entries = this.getEntriesByName(entryName, "mark"); + + if (!entries.length) { + throw new Error(`No marks with the name ${entryName}`); + } + + let mostRecentEntry = entries[entries.length - 1]; + return this._perf.timeOrigin + mostRecentEntry.startTime; + }, +}; + +export const perfService = new _PerfService(); diff --git a/browser/components/newtab/content-src/lib/screenshot-utils.js b/browser/components/newtab/content-src/lib/screenshot-utils.js deleted file mode 100644 index 7ea93f12ae..0000000000 --- a/browser/components/newtab/content-src/lib/screenshot-utils.js +++ /dev/null @@ -1,61 +0,0 @@ -/* 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/. */ - -/** - * List of helper functions for screenshot-based images. - * - * There are two kinds of images: - * 1. Remote Image: This is the image from the main process and it refers to - * the image in the React props. This can either be an object with the `data` - * and `path` properties, if it is a blob, or a string, if it is a normal image. - * 2. Local Image: This is the image object in the content process and it refers - * to the image *object* in the React component's state. All local image - * objects have the `url` property, and an additional property `path`, if they - * are blobs. - */ -export const ScreenshotUtils = { - isBlob(isLocal, image) { - 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) { - return null; - } - if (this.isBlob(false, remoteImage)) { - return { - url: global.URL.createObjectURL(remoteImage.data), - path: remoteImage.path, - }; - } - 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)) { - global.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; - } - - // This will only handle the remaining three possible outcomes. - // (i.e. everything except when both image and localImage are present) - return !remoteImage && !localImage; - }, -}; diff --git a/browser/components/newtab/content-src/lib/screenshot-utils.mjs b/browser/components/newtab/content-src/lib/screenshot-utils.mjs new file mode 100644 index 0000000000..2d1342be4f --- /dev/null +++ b/browser/components/newtab/content-src/lib/screenshot-utils.mjs @@ -0,0 +1,61 @@ +/* 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/. */ + +/** + * List of helper functions for screenshot-based images. + * + * There are two kinds of images: + * 1. Remote Image: This is the image from the main process and it refers to + * the image in the React props. This can either be an object with the `data` + * and `path` properties, if it is a blob, or a string, if it is a normal image. + * 2. Local Image: This is the image object in the content process and it refers + * to the image *object* in the React component's state. All local image + * objects have the `url` property, and an additional property `path`, if they + * are blobs. + */ +export const ScreenshotUtils = { + isBlob(isLocal, image) { + 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) { + return null; + } + if (this.isBlob(false, remoteImage)) { + return { + url: globalThis.URL.createObjectURL(remoteImage.data), + path: remoteImage.path, + }; + } + 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)) { + 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; + } + + // This will only handle the remaining three possible outcomes. + // (i.e. everything except when both image and localImage are present) + return !remoteImage && !localImage; + }, +}; diff --git a/browser/components/newtab/content-src/lib/selectLayoutRender.js b/browser/components/newtab/content-src/lib/selectLayoutRender.js deleted file mode 100644 index 8ef4dd428f..0000000000 --- a/browser/components/newtab/content-src/lib/selectLayoutRender.js +++ /dev/null @@ -1,255 +0,0 @@ -/* 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/. */ - -export const selectLayoutRender = ({ state = {}, prefs = {} }) => { - const { layout, feeds, spocs } = state; - let spocIndexPlacementMap = {}; - - /* This function fills spoc positions on a per placement basis with available spocs. - * It does this by looping through each position for a placement and replacing a rec with a spoc. - * If it runs out of spocs or positions, it stops. - * 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 - ) { - spocIndexPlacementMap[placementName] = 0; - } - const results = [...data]; - for (let position of spocsConfig.positions) { - const spoc = spocsData[spocIndexPlacementMap[placementName]]; - // If there are no spocs left, we can stop filling positions. - if (!spoc) { - break; - } - - // A placement could be used in two sections. - // In these cases, we want to maintain the index of the previous section. - // If we didn't do this, it might duplicate spocs. - spocIndexPlacementMap[placementName]++; - - // A spoc that's blocked is removed from the source for subsequent newtab loads. - // If we have a spoc in the source that's blocked, it means it was *just* blocked, - // and in this case, we skip this position, and show a regular spoc instead. - if (!spocs.blocked.includes(spoc.url)) { - results.splice(position.index, 0, spoc); - } - } - - return results; - } - - const positions = {}; - 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"]; - 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: [], - }, - }; - } - const data = { - 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 }); - } - - 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 - ) { - 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 - ); - } - } - return result; - }; - - const handleComponent = component => { - 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 - ) { - return { - ...component, - data: { - spocs: spocsData.items - .filter(spoc => spoc && !spocs.blocked.includes(spoc.url)) - .map((spoc, index) => ({ - ...spoc, - pos: index, - })), - }, - }; - } - } - return { - ...component, - data: { - spocs: [], - }, - }; - }; - - const handleComponentWithFeed = component => { - positions[component.type] = positions[component.type] || 0; - let data = { - recommendations: [], - }; - - const feed = feeds.data[component.feed.url]; - if (feed && feed.data) { - data = { - ...feed.data, - recommendations: [...(feed.data.recommendations || [])], - }; - } - - if (component && component.properties && component.properties.offset) { - data = { - ...data, - recommendations: data.recommendations.slice( - component.properties.offset - ), - }; - } - - data = { - ...data, - recommendations: handleSpocs(data.recommendations, component), - }; - - let items = 0; - if (component.properties && component.properties.items) { - items = Math.min(component.properties.items, data.recommendations.length); - } - - // loop through a component items - // Store the items position sequentially for multiple components of the same type. - // Example: A second card grid starts pos offset from the last card grid. - for (let i = 0; i < items; i++) { - data.recommendations[i] = { - ...data.recommendations[i], - pos: positions[component.type]++, - }; - } - - return { ...component, data }; - }; - - const renderLayout = () => { - const renderedLayoutArray = []; - for (const row of layout.filter( - r => r.components.filter(c => !filterArray.includes(c.type)).length - )) { - let components = []; - renderedLayoutArray.push({ - ...row, - components, - }); - 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) - ) { - components.push(placeholderComponent(component)); - return renderedLayoutArray; - } - if (component.feed) { - components.push(handleComponentWithFeed(component)); - } else { - components.push(handleComponent(component)); - } - } else { - components.push(component); - } - } - } - return renderedLayoutArray; - }; - - const layoutRender = renderLayout(); - - return { layoutRender }; -}; diff --git a/browser/components/newtab/content-src/lib/selectLayoutRender.mjs b/browser/components/newtab/content-src/lib/selectLayoutRender.mjs new file mode 100644 index 0000000000..8ef4dd428f --- /dev/null +++ b/browser/components/newtab/content-src/lib/selectLayoutRender.mjs @@ -0,0 +1,255 @@ +/* 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/. */ + +export const selectLayoutRender = ({ state = {}, prefs = {} }) => { + const { layout, feeds, spocs } = state; + let spocIndexPlacementMap = {}; + + /* This function fills spoc positions on a per placement basis with available spocs. + * It does this by looping through each position for a placement and replacing a rec with a spoc. + * If it runs out of spocs or positions, it stops. + * 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 + ) { + spocIndexPlacementMap[placementName] = 0; + } + const results = [...data]; + for (let position of spocsConfig.positions) { + const spoc = spocsData[spocIndexPlacementMap[placementName]]; + // If there are no spocs left, we can stop filling positions. + if (!spoc) { + break; + } + + // A placement could be used in two sections. + // In these cases, we want to maintain the index of the previous section. + // If we didn't do this, it might duplicate spocs. + spocIndexPlacementMap[placementName]++; + + // A spoc that's blocked is removed from the source for subsequent newtab loads. + // If we have a spoc in the source that's blocked, it means it was *just* blocked, + // and in this case, we skip this position, and show a regular spoc instead. + if (!spocs.blocked.includes(spoc.url)) { + results.splice(position.index, 0, spoc); + } + } + + return results; + } + + const positions = {}; + 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"]; + 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: [], + }, + }; + } + const data = { + 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 }); + } + + 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 + ) { + 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 + ); + } + } + return result; + }; + + const handleComponent = component => { + 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 + ) { + return { + ...component, + data: { + spocs: spocsData.items + .filter(spoc => spoc && !spocs.blocked.includes(spoc.url)) + .map((spoc, index) => ({ + ...spoc, + pos: index, + })), + }, + }; + } + } + return { + ...component, + data: { + spocs: [], + }, + }; + }; + + const handleComponentWithFeed = component => { + positions[component.type] = positions[component.type] || 0; + let data = { + recommendations: [], + }; + + const feed = feeds.data[component.feed.url]; + if (feed && feed.data) { + data = { + ...feed.data, + recommendations: [...(feed.data.recommendations || [])], + }; + } + + if (component && component.properties && component.properties.offset) { + data = { + ...data, + recommendations: data.recommendations.slice( + component.properties.offset + ), + }; + } + + data = { + ...data, + recommendations: handleSpocs(data.recommendations, component), + }; + + let items = 0; + if (component.properties && component.properties.items) { + items = Math.min(component.properties.items, data.recommendations.length); + } + + // loop through a component items + // Store the items position sequentially for multiple components of the same type. + // Example: A second card grid starts pos offset from the last card grid. + for (let i = 0; i < items; i++) { + data.recommendations[i] = { + ...data.recommendations[i], + pos: positions[component.type]++, + }; + } + + return { ...component, data }; + }; + + const renderLayout = () => { + const renderedLayoutArray = []; + for (const row of layout.filter( + r => r.components.filter(c => !filterArray.includes(c.type)).length + )) { + let components = []; + renderedLayoutArray.push({ + ...row, + components, + }); + 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) + ) { + components.push(placeholderComponent(component)); + return renderedLayoutArray; + } + if (component.feed) { + components.push(handleComponentWithFeed(component)); + } else { + components.push(handleComponent(component)); + } + } else { + components.push(component); + } + } + } + return renderedLayoutArray; + }; + + const layoutRender = renderLayout(); + + return { layoutRender }; +}; 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'; -- cgit v1.2.3