From d57c2801d9dbe47719222c6ffb2d8c13a5dc0006 Mon Sep 17 00:00:00 2001 From: Nathan Barrett Date: Wed, 10 Apr 2024 22:42:12 +0000 Subject: [PATCH] Bug 1881588 - Add Wallpaper component r=home-newtab-reviewers,fluent-reviewers,bolsson,thecount,maxxcrawford Differential Revision: https://phabricator.services.mozilla.com/D205373 --- browser/app/profile/firefox.js | 3 + browser/components/newtab/common/Actions.mjs | 1 + browser/components/newtab/common/Reducers.sys.mjs | 13 ++ .../newtab/content-src/components/Base/Base.jsx | 61 ++++++++ .../newtab/content-src/components/Base/_Base.scss | 38 ++++- .../ContentSection/ContentSection.jsx | 13 ++ .../components/CustomizeMenu/CustomizeMenu.jsx | 4 +- .../components/CustomizeMenu/_CustomizeMenu.scss | 4 + .../WallpapersSection/WallpapersSection.jsx | 93 ++++++++++++ .../WallpapersSection/_WallpapersSection.scss | 82 ++++++++++ .../content-src/styles/_activity-stream.scss | 7 + .../newtab/css/activity-stream-linux.css | 141 +++++++++++++++++- .../components/newtab/css/activity-stream-mac.css | 141 +++++++++++++++++- .../newtab/css/activity-stream-windows.css | 141 +++++++++++++++++- .../newtab/data/content/activity-stream.bundle.js | 165 +++++++++++++++++++-- browser/components/newtab/karma.mc.config.js | 18 +++ .../components/newtab/lib/ActivityStream.sys.mjs | 21 +++ .../components/newtab/lib/WallpaperFeed.sys.mjs | 114 ++++++++++++++ .../newtab/test/xpcshell/test_WallpaperFeed.js | 111 ++++++++++++++ .../components/newtab/test/xpcshell/xpcshell.toml | 2 + browser/locales/en-US/browser/newtab/newtab.ftl | 22 +++ toolkit/components/nimbus/FeatureManifest.yaml | 7 + 22 files changed, 1187 insertions(+), 15 deletions(-) create mode 100644 browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx create mode 100644 browser/components/newtab/content-src/components/WallpapersSection/_WallpapersSection.scss create mode 100644 browser/components/newtab/lib/WallpaperFeed.sys.mjs create mode 100644 browser/components/newtab/test/xpcshell/test_WallpaperFeed.js diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index fa2cd7f51b94..18ff4e13ddeb 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1692,6 +1692,9 @@ pref("browser.partnerlink.campaign.topsites", "amzn_2020_a1"); // Activates preloading of the new tab url. pref("browser.newtab.preload", true); +// Preference to enable wallpaper selection in the Customize Menu of new tab page +pref("browser.newtabpage.activity-stream.newtabWallpapers.enabled", false); + // Current new tab page background image. pref("browser.newtabpage.activity-stream.newtabWallpapers.wallpaper", ""); diff --git a/browser/components/newtab/common/Actions.mjs b/browser/components/newtab/common/Actions.mjs index 490fa595073b..7273d80220fc 100644 --- a/browser/components/newtab/common/Actions.mjs +++ b/browser/components/newtab/common/Actions.mjs @@ -160,6 +160,7 @@ for (const type of [ "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", + "WALLPAPERS_SET", "WEBEXT_CLICK", "WEBEXT_DISMISS", ]) { diff --git a/browser/components/newtab/common/Reducers.sys.mjs b/browser/components/newtab/common/Reducers.sys.mjs index f01c6d6efdd5..326217538ddd 100644 --- a/browser/components/newtab/common/Reducers.sys.mjs +++ b/browser/components/newtab/common/Reducers.sys.mjs @@ -101,6 +101,9 @@ export const INITIAL_STATE = { // Hide the search box after handing off to AwesomeBar and user starts typing. hide: false, }, + Wallpapers: { + wallpaperList: [], + }, }; function App(prevState = INITIAL_STATE.App, action) { @@ -841,6 +844,15 @@ function Search(prevState = INITIAL_STATE.Search, action) { } } +function Wallpapers(prevState = INITIAL_STATE.Wallpapers, action) { + switch (action.type) { + case at.WALLPAPERS_SET: + return { wallpaperList: action.data }; + default: + return prevState; + } +} + export const reducers = { TopSites, App, @@ -852,4 +864,5 @@ export const reducers = { Personalization, DiscoveryStream, Search, + Wallpapers, }; diff --git a/browser/components/newtab/content-src/components/Base/Base.jsx b/browser/components/newtab/content-src/components/Base/Base.jsx index c7eab8f5cb86..64060a1e2c03 100644 --- a/browser/components/newtab/content-src/components/Base/Base.jsx +++ b/browser/components/newtab/content-src/components/Base/Base.jsx @@ -110,6 +110,7 @@ export class BaseContent extends React.PureComponent { this.handleOnKeyDown = this.handleOnKeyDown.bind(this); this.onWindowScroll = debounce(this.onWindowScroll.bind(this), 5); this.setPref = this.setPref.bind(this); + this.updateWallpaper = this.updateWallpaper.bind(this); this.state = { fixedSearch: false, firstVisibleTimestamp: null }; } @@ -192,11 +193,64 @@ export class BaseContent extends React.PureComponent { this.props.dispatch(ac.SetPref(pref, value)); } + renderWallpaperAttribution() { + const activeWallpaper = + this.props.Prefs.values["newtabWallpapers.wallpaper"]; + const { wallpaperList } = this.props.Wallpapers; + const selected = wallpaperList.find(wp => wp.title === activeWallpaper); + // make sure a wallpaper is selected and that the attribution also exists + if (!selected?.attribution) { + return null; + } + + const { name, webpage } = selected.attribution; + if (activeWallpaper && wallpaperList && name.url) { + return ( +

+ + {name.string} + + + {webpage.string} + +

+ ); + } + return null; + } + + async updateWallpaper() { + const prefs = this.props.Prefs.values; + const activeWallpaper = prefs["newtabWallpapers.wallpaper"]; + const { wallpaperList } = this.props.Wallpapers; + if (wallpaperList) { + const wallpaper = + wallpaperList.find(wp => wp.title === activeWallpaper) || ""; + global.document?.body.style.setProperty( + "--newtab-wallpaper", + `url(${wallpaper?.wallpaperUrl || ""})` + ); + } + } + render() { const { props } = this; const { App } = props; const { initialized, customizeMenuVisible } = App; const prefs = props.Prefs.values; + const activeWallpaper = prefs["newtabWallpapers.wallpaper"]; + const wallpapersEnabled = prefs["newtabWallpapers.enabled"]; + const { pocketConfig } = prefs; const isDiscoveryStream = @@ -247,6 +301,9 @@ export class BaseContent extends React.PureComponent { ] .filter(v => v) .join(" "); + if (wallpapersEnabled) { + this.updateWallpaper(); + } return (
@@ -256,6 +313,8 @@ export class BaseContent extends React.PureComponent { openPreferences={this.openPreferences} setPref={this.setPref} enabledSections={enabledSections} + wallpapersEnabled={wallpapersEnabled} + activeWallpaper={activeWallpaper} pocketRegion={pocketRegion} mayHaveSponsoredTopSites={mayHaveSponsoredTopSites} mayHaveSponsoredStories={mayHaveSponsoredStories} @@ -292,6 +351,7 @@ export class BaseContent extends React.PureComponent { )}
+ {wallpapersEnabled && this.renderWallpaperAttribution()} @@ -309,4 +369,5 @@ export const Base = connect(state => ({ 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 1282173df510..a9141e0923ad 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/CustomizeMenu/ContentSection/ContentSection.jsx b/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx index e1d13b55b5a9..1dd13fc965e6 100644 --- a/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx +++ b/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx @@ -5,6 +5,7 @@ import React from "react"; 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)} /> - { + colorTheme.addEventListener("change", this.handleReset); + } + ); + } + + componentWillUnmount() { + [this.prefersHighContrastQuery, this.prefersDarkQuery].forEach( + colorTheme => { + colorTheme.removeEventListener("change", this.handleReset); + } + ); + } + + handleChange(event) { + const { id } = event.target; + this.props.setPref("newtabWallpapers.wallpaper", id); + } + + handleReset() { + this.props.setPref("newtabWallpapers.wallpaper", "i"); + } + + 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 000000000000..8bdb7a3cdddf --- /dev/null +++ b/browser/components/newtab/content-src/components/WallpapersSection/_WallpapersSection.scss @@ -0,0 +1,82 @@ +.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-aurora, dark-city, dark-color, dark-mountain, dark-panda, dark-sky, light-beach, light-color, light-landscape, light-mountain, light-redpanda, light-sky; + + @each $wallpaper in $wallpapers { + &.#{$wallpaper} { + background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/#{$wallpaper}.avif') + } + } + + &:checked { + 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; + } +} + +.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/styles/_activity-stream.scss b/browser/components/newtab/content-src/styles/_activity-stream.scss index 88ed530b6ab5..f75aa068e571 100644 --- a/browser/components/newtab/content-src/styles/_activity-stream.scss +++ b/browser/components/newtab/content-src/styles/_activity-stream.scss @@ -21,6 +21,12 @@ 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-image: var(--newtab-wallpaper, ''); + background-repeat: no-repeat; + background-size: cover; + background-position: center; + background-attachment: fixed; } .no-scroll { @@ -137,6 +143,7 @@ input { @import '../components/ContextMenu/ContextMenu'; @import '../components/ConfirmDialog/ConfirmDialog'; @import '../components/CustomizeMenu/CustomizeMenu'; +@import '../components/WallpapersSection/WallpapersSection'; @import '../components/Card/Card'; @import '../components/CollapsibleSection/CollapsibleSection'; @import '../components/DiscoveryStreamAdmin/DiscoveryStreamAdmin'; diff --git a/browser/components/newtab/css/activity-stream-linux.css b/browser/components/newtab/css/activity-stream-linux.css index 87731597379f..ad97064daebc 100644 --- a/browser/components/newtab/css/activity-stream-linux.css +++ b/browser/components/newtab/css/activity-stream-linux.css @@ -276,6 +276,11 @@ body { background-color: var(--newtab-background-color); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Ubuntu, "Helvetica Neue", sans-serif; font-size: 16px; + background-image: var(--newtab-wallpaper, ""); + background-repeat: no-repeat; + background-size: cover; + background-position: center; + background-attachment: fixed; } .no-scroll { @@ -405,10 +410,16 @@ input[type=text], input[type=search] { } main { - margin: auto; + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; width: 274px; padding: 0; } +main .vertical-center-wrapper { + margin: auto 0; +} main section { margin-bottom: 20px; position: relative; @@ -489,6 +500,29 @@ main section { background-color: var(--newtab-element-active-color); } +.wallpaper-attribution { + padding: 0 25px; + font-size: 14px; +} +.wallpaper-attribution.theme-light { + display: inline-block; +} +[lwt-newtab-brighttext] .wallpaper-attribution.theme-light { + display: none; +} +.wallpaper-attribution.theme-dark { + display: none; +} +[lwt-newtab-brighttext] .wallpaper-attribution.theme-dark { + display: inline-block; +} +.wallpaper-attribution a { + color: var(--newtab-element-color); +} +.wallpaper-attribution a:hover { + text-decoration: none; +} + .as-error-fallback { align-items: center; border-radius: 3px; @@ -1694,6 +1728,9 @@ main section { grid-row-gap: 32px; padding: 0 16px; } +.home-section .wallpapers-section h2 { + font-size: inherit; +} .home-section .section moz-toggle { margin-bottom: 10px; } @@ -1830,6 +1867,108 @@ main section { box-shadow: 0 0 0 2px var(--newtab-primary-action-background-dimmed); } +.wallpaper-list { + display: grid; + gap: 16px; + grid-template-columns: 1fr 1fr 1fr; + grid-auto-rows: 86px; + margin: 16px 0; + padding: 0; + border: none; +} +.wallpaper-list .wallpaper-input.theme-light, +.wallpaper-list .sr-only.theme-light { + display: inline-block; +} +[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-light, +[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-light { + display: none; +} +.wallpaper-list .wallpaper-input.theme-dark, +.wallpaper-list .sr-only.theme-dark { + display: none; +} +[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-dark, +[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-dark { + display: inline-block; +} +.wallpaper-list .wallpaper-input { + appearance: none; + margin: 0; + padding: 0; + height: 86px; + width: 100%; + box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2); + border-radius: 8px; + background-clip: content-box; + background-repeat: no-repeat; + background-size: cover; + cursor: pointer; + outline: 2px solid transparent; +} +.wallpaper-list .wallpaper-input.dark-aurora { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-aurora.avif"); +} +.wallpaper-list .wallpaper-input.dark-city { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-city.avif"); +} +.wallpaper-list .wallpaper-input.dark-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-color.avif"); +} +.wallpaper-list .wallpaper-input.dark-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-mountain.avif"); +} +.wallpaper-list .wallpaper-input.dark-panda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-panda.avif"); +} +.wallpaper-list .wallpaper-input.dark-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-sky.avif"); +} +.wallpaper-list .wallpaper-input.light-beach { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-beach.avif"); +} +.wallpaper-list .wallpaper-input.light-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-color.avif"); +} +.wallpaper-list .wallpaper-input.light-landscape { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-landscape.avif"); +} +.wallpaper-list .wallpaper-input.light-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-mountain.avif"); +} +.wallpaper-list .wallpaper-input.light-redpanda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-redpanda.avif"); +} +.wallpaper-list .wallpaper-input.light-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-sky.avif"); +} +.wallpaper-list .wallpaper-input:checked { + outline-color: var(--newtab-primary-action-background); +} +.wallpaper-list .wallpaper-input:hover { + filter: brightness(55%); + outline-color: transparent; +} +.wallpaper-list .sr-only { + opacity: 0; + overflow: hidden; + position: absolute; +} + +.wallpapers-reset { + background: none; + border: none; + text-decoration: underline; + margin-inline: auto; + display: block; + font-size: var(--font-size-small); + color: var(--newtab-text-primary-color); + cursor: pointer; +} +.wallpapers-reset:hover { + text-decoration: none; +} + /* stylelint-disable max-nesting-depth */ .card-outer { background: var(--newtab-background-color-secondary); diff --git a/browser/components/newtab/css/activity-stream-mac.css b/browser/components/newtab/css/activity-stream-mac.css index 87b942818a06..b1260c3b0143 100644 --- a/browser/components/newtab/css/activity-stream-mac.css +++ b/browser/components/newtab/css/activity-stream-mac.css @@ -280,6 +280,11 @@ body { background-color: var(--newtab-background-color); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Ubuntu, "Helvetica Neue", sans-serif; font-size: 16px; + background-image: var(--newtab-wallpaper, ""); + background-repeat: no-repeat; + background-size: cover; + background-position: center; + background-attachment: fixed; } .no-scroll { @@ -409,10 +414,16 @@ input[type=text], input[type=search] { } main { - margin: auto; + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; width: 274px; padding: 0; } +main .vertical-center-wrapper { + margin: auto 0; +} main section { margin-bottom: 20px; position: relative; @@ -493,6 +504,29 @@ main section { background-color: var(--newtab-element-active-color); } +.wallpaper-attribution { + padding: 0 25px; + font-size: 14px; +} +.wallpaper-attribution.theme-light { + display: inline-block; +} +[lwt-newtab-brighttext] .wallpaper-attribution.theme-light { + display: none; +} +.wallpaper-attribution.theme-dark { + display: none; +} +[lwt-newtab-brighttext] .wallpaper-attribution.theme-dark { + display: inline-block; +} +.wallpaper-attribution a { + color: var(--newtab-element-color); +} +.wallpaper-attribution a:hover { + text-decoration: none; +} + .as-error-fallback { align-items: center; border-radius: 3px; @@ -1698,6 +1732,9 @@ main section { grid-row-gap: 32px; padding: 0 16px; } +.home-section .wallpapers-section h2 { + font-size: inherit; +} .home-section .section moz-toggle { margin-bottom: 10px; } @@ -1834,6 +1871,108 @@ main section { box-shadow: 0 0 0 2px var(--newtab-primary-action-background-dimmed); } +.wallpaper-list { + display: grid; + gap: 16px; + grid-template-columns: 1fr 1fr 1fr; + grid-auto-rows: 86px; + margin: 16px 0; + padding: 0; + border: none; +} +.wallpaper-list .wallpaper-input.theme-light, +.wallpaper-list .sr-only.theme-light { + display: inline-block; +} +[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-light, +[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-light { + display: none; +} +.wallpaper-list .wallpaper-input.theme-dark, +.wallpaper-list .sr-only.theme-dark { + display: none; +} +[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-dark, +[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-dark { + display: inline-block; +} +.wallpaper-list .wallpaper-input { + appearance: none; + margin: 0; + padding: 0; + height: 86px; + width: 100%; + box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2); + border-radius: 8px; + background-clip: content-box; + background-repeat: no-repeat; + background-size: cover; + cursor: pointer; + outline: 2px solid transparent; +} +.wallpaper-list .wallpaper-input.dark-aurora { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-aurora.avif"); +} +.wallpaper-list .wallpaper-input.dark-city { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-city.avif"); +} +.wallpaper-list .wallpaper-input.dark-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-color.avif"); +} +.wallpaper-list .wallpaper-input.dark-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-mountain.avif"); +} +.wallpaper-list .wallpaper-input.dark-panda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-panda.avif"); +} +.wallpaper-list .wallpaper-input.dark-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-sky.avif"); +} +.wallpaper-list .wallpaper-input.light-beach { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-beach.avif"); +} +.wallpaper-list .wallpaper-input.light-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-color.avif"); +} +.wallpaper-list .wallpaper-input.light-landscape { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-landscape.avif"); +} +.wallpaper-list .wallpaper-input.light-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-mountain.avif"); +} +.wallpaper-list .wallpaper-input.light-redpanda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-redpanda.avif"); +} +.wallpaper-list .wallpaper-input.light-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-sky.avif"); +} +.wallpaper-list .wallpaper-input:checked { + outline-color: var(--newtab-primary-action-background); +} +.wallpaper-list .wallpaper-input:hover { + filter: brightness(55%); + outline-color: transparent; +} +.wallpaper-list .sr-only { + opacity: 0; + overflow: hidden; + position: absolute; +} + +.wallpapers-reset { + background: none; + border: none; + text-decoration: underline; + margin-inline: auto; + display: block; + font-size: var(--font-size-small); + color: var(--newtab-text-primary-color); + cursor: pointer; +} +.wallpapers-reset:hover { + text-decoration: none; +} + /* stylelint-disable max-nesting-depth */ .card-outer { background: var(--newtab-background-color-secondary); diff --git a/browser/components/newtab/css/activity-stream-windows.css b/browser/components/newtab/css/activity-stream-windows.css index 25370fdf1915..e85a33e2de51 100644 --- a/browser/components/newtab/css/activity-stream-windows.css +++ b/browser/components/newtab/css/activity-stream-windows.css @@ -276,6 +276,11 @@ body { background-color: var(--newtab-background-color); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Ubuntu, "Helvetica Neue", sans-serif; font-size: 16px; + background-image: var(--newtab-wallpaper, ""); + background-repeat: no-repeat; + background-size: cover; + background-position: center; + background-attachment: fixed; } .no-scroll { @@ -405,10 +410,16 @@ input[type=text], input[type=search] { } main { - margin: auto; + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; width: 274px; padding: 0; } +main .vertical-center-wrapper { + margin: auto 0; +} main section { margin-bottom: 20px; position: relative; @@ -489,6 +500,29 @@ main section { background-color: var(--newtab-element-active-color); } +.wallpaper-attribution { + padding: 0 25px; + font-size: 14px; +} +.wallpaper-attribution.theme-light { + display: inline-block; +} +[lwt-newtab-brighttext] .wallpaper-attribution.theme-light { + display: none; +} +.wallpaper-attribution.theme-dark { + display: none; +} +[lwt-newtab-brighttext] .wallpaper-attribution.theme-dark { + display: inline-block; +} +.wallpaper-attribution a { + color: var(--newtab-element-color); +} +.wallpaper-attribution a:hover { + text-decoration: none; +} + .as-error-fallback { align-items: center; border-radius: 3px; @@ -1694,6 +1728,9 @@ main section { grid-row-gap: 32px; padding: 0 16px; } +.home-section .wallpapers-section h2 { + font-size: inherit; +} .home-section .section moz-toggle { margin-bottom: 10px; } @@ -1830,6 +1867,108 @@ main section { box-shadow: 0 0 0 2px var(--newtab-primary-action-background-dimmed); } +.wallpaper-list { + display: grid; + gap: 16px; + grid-template-columns: 1fr 1fr 1fr; + grid-auto-rows: 86px; + margin: 16px 0; + padding: 0; + border: none; +} +.wallpaper-list .wallpaper-input.theme-light, +.wallpaper-list .sr-only.theme-light { + display: inline-block; +} +[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-light, +[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-light { + display: none; +} +.wallpaper-list .wallpaper-input.theme-dark, +.wallpaper-list .sr-only.theme-dark { + display: none; +} +[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-dark, +[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-dark { + display: inline-block; +} +.wallpaper-list .wallpaper-input { + appearance: none; + margin: 0; + padding: 0; + height: 86px; + width: 100%; + box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2); + border-radius: 8px; + background-clip: content-box; + background-repeat: no-repeat; + background-size: cover; + cursor: pointer; + outline: 2px solid transparent; +} +.wallpaper-list .wallpaper-input.dark-aurora { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-aurora.avif"); +} +.wallpaper-list .wallpaper-input.dark-city { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-city.avif"); +} +.wallpaper-list .wallpaper-input.dark-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-color.avif"); +} +.wallpaper-list .wallpaper-input.dark-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-mountain.avif"); +} +.wallpaper-list .wallpaper-input.dark-panda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-panda.avif"); +} +.wallpaper-list .wallpaper-input.dark-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-sky.avif"); +} +.wallpaper-list .wallpaper-input.light-beach { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-beach.avif"); +} +.wallpaper-list .wallpaper-input.light-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-color.avif"); +} +.wallpaper-list .wallpaper-input.light-landscape { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-landscape.avif"); +} +.wallpaper-list .wallpaper-input.light-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-mountain.avif"); +} +.wallpaper-list .wallpaper-input.light-redpanda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-redpanda.avif"); +} +.wallpaper-list .wallpaper-input.light-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-sky.avif"); +} +.wallpaper-list .wallpaper-input:checked { + outline-color: var(--newtab-primary-action-background); +} +.wallpaper-list .wallpaper-input:hover { + filter: brightness(55%); + outline-color: transparent; +} +.wallpaper-list .sr-only { + opacity: 0; + overflow: hidden; + position: absolute; +} + +.wallpapers-reset { + background: none; + border: none; + text-decoration: underline; + margin-inline: auto; + display: block; + font-size: var(--font-size-small); + color: var(--newtab-text-primary-color); + cursor: pointer; +} +.wallpapers-reset:hover { + text-decoration: none; +} + /* stylelint-disable max-nesting-depth */ .card-outer { background: var(--newtab-background-color-secondary); diff --git a/browser/components/newtab/data/content/activity-stream.bundle.js b/browser/components/newtab/data/content/activity-stream.bundle.js index 117a7b1d4e02..579982e67770 100644 --- a/browser/components/newtab/data/content/activity-stream.bundle.js +++ b/browser/components/newtab/data/content/activity-stream.bundle.js @@ -233,6 +233,7 @@ for (const type of [ "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", + "WALLPAPERS_SET", "WEBEXT_CLICK", "WEBEXT_DISMISS", ]) { @@ -5530,6 +5531,9 @@ const INITIAL_STATE = { // Hide the search box after handing off to AwesomeBar and user starts typing. hide: false, }, + Wallpapers: { + wallpaperList: [], + }, }; function App(prevState = INITIAL_STATE.App, action) { @@ -6270,6 +6274,15 @@ function Search(prevState = INITIAL_STATE.Search, action) { } } +function Wallpapers(prevState = INITIAL_STATE.Wallpapers, action) { + switch (action.type) { + case actionTypes.WALLPAPERS_SET: + return { wallpaperList: action.data }; + default: + return prevState; + } +} + const reducers = { TopSites, App, @@ -6281,6 +6294,7 @@ const reducers = { Personalization: Reducers_sys_Personalization, DiscoveryStream, Search, + Wallpapers, }; ;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteFormInput.jsx @@ -8833,17 +8847,83 @@ const DiscoveryStreamBase = (0,external_ReactRedux_namespaceObject.connect)(stat document: globalThis.document, App: state.App }))(_DiscoveryStreamBase); -;// CONCATENATED MODULE: ./content-src/components/CustomizeMenu/BackgroundsSection/BackgroundsSection.jsx +;// CONCATENATED MODULE: ./content-src/components/WallpapersSection/WallpapersSection.jsx /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -class BackgroundsSection extends (external_React_default()).PureComponent { + +class _WallpapersSection extends (external_React_default()).PureComponent { + constructor(props) { + super(props); + this.handleChange = this.handleChange.bind(this); + this.handleReset = this.handleReset.bind(this); + this.prefersHighContrastQuery = null; + this.prefersDarkQuery = null; + } + componentDidMount() { + this.prefersHighContrastQuery = globalThis.matchMedia("(forced-colors: active)"); + this.prefersDarkQuery = globalThis.matchMedia("(prefers-color-scheme: dark)"); + [this.prefersHighContrastQuery, this.prefersDarkQuery].forEach(colorTheme => { + colorTheme.addEventListener("change", this.handleReset); + }); + } + componentWillUnmount() { + [this.prefersHighContrastQuery, this.prefersDarkQuery].forEach(colorTheme => { + colorTheme.removeEventListener("change", this.handleReset); + }); + } + handleChange(event) { + const { + id + } = event.target; + this.props.setPref("newtabWallpapers.wallpaper", id); + } + handleReset() { + this.props.setPref("newtabWallpapers.wallpaper", "i"); + } render() { - return /*#__PURE__*/external_React_default().createElement("div", null); + const { + wallpaperList + } = this.props.Wallpapers; + const { + activeWallpaper + } = this.props; + return /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement("fieldset", { + className: "wallpaper-list" + }, wallpaperList.map(({ + title, + theme, + fluent_id + }) => { + return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("input", { + onChange: this.handleChange, + type: "radio", + name: "wallpaper", + id: title, + value: title, + checked: title === activeWallpaper, + "aria-checked": title === activeWallpaper, + className: `wallpaper-input theme-${theme} ${title}` + }), /*#__PURE__*/external_React_default().createElement("label", { + htmlFor: title, + className: "sr-only", + "data-l10n-id": fluent_id + }, fluent_id)); + })), /*#__PURE__*/external_React_default().createElement("button", { + className: "wallpapers-reset", + onClick: this.handleReset, + "data-l10n-id": "newtab-wallpaper-reset" + })); } } +const WallpapersSection = (0,external_ReactRedux_namespaceObject.connect)(state => { + return { + Wallpapers: state.Wallpapers, + Prefs: state.Prefs + }; +})(_WallpapersSection); ;// CONCATENATED MODULE: ./content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, @@ -8852,6 +8932,7 @@ class BackgroundsSection extends (external_React_default()).PureComponent { + class ContentSection extends (external_React_default()).PureComponent { constructor(props) { super(props); @@ -8930,7 +9011,10 @@ class ContentSection extends (external_React_default()).PureComponent { mayHaveSponsoredStories, mayHaveRecentSaves, openPreferences, - spocMessageVariant + spocMessageVariant, + wallpapersEnabled, + activeWallpaper, + setPref } = this.props; const { topSitesEnabled, @@ -8943,7 +9027,14 @@ class ContentSection extends (external_React_default()).PureComponent { } = enabledSections; return /*#__PURE__*/external_React_default().createElement("div", { className: "home-section" - }, /*#__PURE__*/external_React_default().createElement("div", { + }, wallpapersEnabled && /*#__PURE__*/external_React_default().createElement("div", { + className: "wallpapers-section" + }, /*#__PURE__*/external_React_default().createElement("h2", { + "data-l10n-id": "newtab-wallpaper-title" + }), /*#__PURE__*/external_React_default().createElement(WallpapersSection, { + setPref: setPref, + activeWallpaper: activeWallpaper + })), /*#__PURE__*/external_React_default().createElement("div", { id: "shortcuts-section", className: "section" }, /*#__PURE__*/external_React_default().createElement("moz-toggle", { @@ -9091,7 +9182,6 @@ class ContentSection extends (external_React_default()).PureComponent { - class _CustomizeMenu extends (external_React_default()).PureComponent { constructor(props) { super(props); @@ -9135,10 +9225,12 @@ class _CustomizeMenu extends (external_React_default()).PureComponent { className: "close-button", "data-l10n-id": "newtab-custom-close-button", ref: c => this.closeButton = c - }), /*#__PURE__*/external_React_default().createElement(BackgroundsSection, null), /*#__PURE__*/external_React_default().createElement(ContentSection, { + }), /*#__PURE__*/external_React_default().createElement(ContentSection, { openPreferences: this.props.openPreferences, setPref: this.props.setPref, enabledSections: this.props.enabledSections, + wallpapersEnabled: this.props.wallpapersEnabled, + activeWallpaper: this.props.activeWallpaper, pocketRegion: this.props.pocketRegion, mayHaveSponsoredTopSites: this.props.mayHaveSponsoredTopSites, mayHaveSponsoredStories: this.props.mayHaveSponsoredStories, @@ -9453,6 +9545,7 @@ class BaseContent extends (external_React_default()).PureComponent { this.handleOnKeyDown = this.handleOnKeyDown.bind(this); this.onWindowScroll = debounce(this.onWindowScroll.bind(this), 5); this.setPref = this.setPref.bind(this); + this.updateWallpaper = this.updateWallpaper.bind(this); this.state = { fixedSearch: false, firstVisibleTimestamp: null @@ -9535,6 +9628,52 @@ class BaseContent extends (external_React_default()).PureComponent { setPref(pref, value) { this.props.dispatch(actionCreators.SetPref(pref, value)); } + renderWallpaperAttribution() { + const activeWallpaper = this.props.Prefs.values["newtabWallpapers.wallpaper"]; + const { + wallpaperList + } = this.props.Wallpapers; + const selected = wallpaperList.find(wp => wp.title === activeWallpaper); + // make sure a wallpaper is selected and that the attribution also exists + if (!selected?.attribution) { + return null; + } + const { + name, + webpage + } = selected.attribution; + if (activeWallpaper && wallpaperList && name.url) { + return /*#__PURE__*/external_React_default().createElement("p", { + className: `wallpaper-attribution`, + key: name, + "data-l10n-id": "newtab-wallpaper-attribution", + "data-l10n-args": JSON.stringify({ + author_string: name.string, + author_url: name.url, + webpage_string: webpage.string, + webpage_url: webpage.url + }) + }, /*#__PURE__*/external_React_default().createElement("a", { + "data-l10n-name": "name-link", + href: name.url + }, name.string), /*#__PURE__*/external_React_default().createElement("a", { + "data-l10n-name": "webpage-link", + href: webpage.url + }, webpage.string)); + } + return null; + } + async updateWallpaper() { + const prefs = this.props.Prefs.values; + const activeWallpaper = prefs["newtabWallpapers.wallpaper"]; + const { + wallpaperList + } = this.props.Wallpapers; + if (wallpaperList) { + const wallpaper = wallpaperList.find(wp => wp.title === activeWallpaper) || ""; + __webpack_require__.g.document?.body.style.setProperty("--newtab-wallpaper", `url(${wallpaper?.wallpaperUrl || ""})`); + } + } render() { const { props @@ -9547,6 +9686,8 @@ class BaseContent extends (external_React_default()).PureComponent { customizeMenuVisible } = App; const prefs = props.Prefs.values; + const activeWallpaper = prefs["newtabWallpapers.wallpaper"]; + const wallpapersEnabled = prefs["newtabWallpapers.enabled"]; const { pocketConfig } = prefs; @@ -9574,12 +9715,17 @@ class BaseContent extends (external_React_default()).PureComponent { mayHaveSponsoredTopSites } = prefs; const outerClassName = ["outer-wrapper", isDiscoveryStream && pocketEnabled && "ds-outer-wrapper-search-alignment", isDiscoveryStream && "ds-outer-wrapper-breakpoint-override", prefs.showSearch && this.state.fixedSearch && !noSectionsEnabled && "fixed-search", prefs.showSearch && noSectionsEnabled && "only-search", prefs["logowordmark.alwaysVisible"] && "visible-logo"].filter(v => v).join(" "); + if (wallpapersEnabled) { + this.updateWallpaper(); + } return /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement(CustomizeMenu, { onClose: this.closeCustomizationMenu, onOpen: this.openCustomizationMenu, openPreferences: this.openPreferences, setPref: this.setPref, enabledSections: enabledSections, + wallpapersEnabled: wallpapersEnabled, + activeWallpaper: activeWallpaper, pocketRegion: pocketRegion, mayHaveSponsoredTopSites: mayHaveSponsoredTopSites, mayHaveSponsoredStories: mayHaveSponsoredStories, @@ -9601,7 +9747,7 @@ class BaseContent extends (external_React_default()).PureComponent { locale: props.App.locale, mayHaveSponsoredStories: mayHaveSponsoredStories, firstVisibleTimestamp: this.state.firstVisibleTimestamp - })) : /*#__PURE__*/external_React_default().createElement(Sections_Sections, null)), /*#__PURE__*/external_React_default().createElement(ConfirmDialog, null)))); + })) : /*#__PURE__*/external_React_default().createElement(Sections_Sections, null)), /*#__PURE__*/external_React_default().createElement(ConfirmDialog, null), wallpapersEnabled && this.renderWallpaperAttribution()))); } } BaseContent.defaultProps = { @@ -9612,7 +9758,8 @@ const Base = (0,external_ReactRedux_namespaceObject.connect)(state => ({ Prefs: state.Prefs, Sections: state.Sections, DiscoveryStream: state.DiscoveryStream, - Search: state.Search + Search: state.Search, + Wallpapers: state.Wallpapers }))(_Base); ;// CONCATENATED MODULE: ./content-src/lib/detect-user-session-start.mjs /* This Source Code Form is subject to the terms of the Mozilla Public diff --git a/browser/components/newtab/karma.mc.config.js b/browser/components/newtab/karma.mc.config.js index 36670ede67dd..886b19df7b11 100644 --- a/browser/components/newtab/karma.mc.config.js +++ b/browser/components/newtab/karma.mc.config.js @@ -158,6 +158,15 @@ module.exports = function (config) { functions: 0, branches: 0, }, + /** + * WallpaperFeed.sys.mjs is tested via an xpcshell test + */ + "lib/WallpaperFeed.sys.mjs": { + statements: 0, + lines: 0, + functions: 0, + branches: 0, + }, "content-src/components/DiscoveryStreamComponents/**/*.jsx": { statements: 90.48, lines: 90.48, @@ -170,6 +179,15 @@ module.exports = function (config) { functions: 60, branches: 50, }, + /** + * WallpaperSection.jsx is tested via an xpcshell test + */ + "content-src/components/WallpapersSection/*.jsx": { + statements: 0, + lines: 0, + functions: 0, + branches: 0, + }, "content-src/components/DiscoveryStreamAdmin/*.jsx": { statements: 0, lines: 0, diff --git a/browser/components/newtab/lib/ActivityStream.sys.mjs b/browser/components/newtab/lib/ActivityStream.sys.mjs index b6c0b5bcaa22..da2b46445a59 100644 --- a/browser/components/newtab/lib/ActivityStream.sys.mjs +++ b/browser/components/newtab/lib/ActivityStream.sys.mjs @@ -36,6 +36,7 @@ ChromeUtils.defineESModuleGetters(lazy, { TelemetryFeed: "resource://activity-stream/lib/TelemetryFeed.sys.mjs", TopSitesFeed: "resource://activity-stream/lib/TopSitesFeed.sys.mjs", TopStoriesFeed: "resource://activity-stream/lib/TopStoriesFeed.sys.mjs", + WallpaperFeed: "resource://activity-stream/lib/WallpaperFeed.sys.mjs", }); // NB: Eagerly load modules that will be loaded/constructed/initialized in the @@ -233,6 +234,20 @@ export const PREFS_CONFIG = new Map([ }, ], [ + "newtabWallpapers.enabled", + { + title: "Boolean flag to turn wallpaper functionality on and off", + value: true, + }, + ], + [ + "newtabWallpapers.wallpaper", + { + title: "Currently set wallpaper", + value: "", + }, + ], + [ "improvesearch.noDefaultSearchTile", { title: "Remove tiles that are the same as the default search", @@ -524,6 +539,12 @@ const FEEDS_DATA = [ title: "Handles new pocket ui for the new tab page", value: true, }, + { + name: "wallpaperfeed", + factory: () => new lazy.WallpaperFeed(), + title: "Handles fetching and managing wallpaper data from RemoteSettings", + value: true, + }, ]; const FEEDS_CONFIG = new Map(); diff --git a/browser/components/newtab/lib/WallpaperFeed.sys.mjs b/browser/components/newtab/lib/WallpaperFeed.sys.mjs new file mode 100644 index 000000000000..b280cce99698 --- /dev/null +++ b/browser/components/newtab/lib/WallpaperFeed.sys.mjs @@ -0,0 +1,114 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + Utils: "resource://services-settings/Utils.sys.mjs", +}); + +import { + actionTypes as at, + actionCreators as ac, +} from "resource://activity-stream/common/Actions.mjs"; + +const PREF_WALLPAPERS_ENABLED = + "browser.newtabpage.activity-stream.newtabWallpapers.enabled"; + +export class WallpaperFeed { + constructor() { + this.loaded = false; + this.wallpaperClient = ""; + this.wallpaperDB = ""; + this.baseAttachmentURL = ""; + } + + /** + * This thin wrapper around global.fetch makes it easier for us to write + * automated tests that simulate responses from this fetch. + */ + fetch(...args) { + return fetch(...args); + } + + /** + * This thin wrapper around lazy.RemoteSettings makes it easier for us to write + * automated tests that simulate responses from this fetch. + */ + RemoteSettings(...args) { + return lazy.RemoteSettings(...args); + } + + async wallpaperSetup() { + const wallpapersEnabled = Services.prefs.getBoolPref( + PREF_WALLPAPERS_ENABLED + ); + + if (wallpapersEnabled) { + if (!this.wallpaperClient) { + this.wallpaperClient = this.RemoteSettings("newtab-wallpapers"); + } + + await this.getBaseAttachment(); + this.wallpaperClient.on("sync", () => this.updateWallpapers()); + this.updateWallpapers(); + } + } + + async getBaseAttachment() { + if (!this.baseAttachmentURL) { + const SERVER = lazy.Utils.SERVER_URL; + const serverInfo = await ( + await this.fetch(`${SERVER}/`, { + credentials: "omit", + }) + ).json(); + const { base_url } = serverInfo.capabilities.attachments; + this.baseAttachmentURL = base_url; + } + } + + async updateWallpapers() { + const records = await this.wallpaperClient.get(); + if (!records?.length) { + return; + } + + if (!this.baseAttachmentURL) { + await this.getBaseAttachment(); + } + const wallpapers = records.map(record => { + return { + ...record, + wallpaperUrl: `${this.baseAttachmentURL}${record.attachment.location}`, + }; + }); + + this.store.dispatch( + ac.BroadcastToContent({ + type: at.WALLPAPERS_SET, + data: wallpapers, + }) + ); + } + + async onAction(action) { + switch (action.type) { + case at.INIT: + await this.wallpaperSetup(); + break; + case at.UNINIT: + break; + case at.SYSTEM_TICK: + break; + case at.PREF_CHANGED: + if (action.data.name === "newtabWallpapers.enabled") { + await this.wallpaperSetup(); + } + break; + case at.WALLPAPERS_SET: + break; + } + } +} diff --git a/browser/components/newtab/test/xpcshell/test_WallpaperFeed.js b/browser/components/newtab/test/xpcshell/test_WallpaperFeed.js new file mode 100644 index 000000000000..48b39243c7a9 --- /dev/null +++ b/browser/components/newtab/test/xpcshell/test_WallpaperFeed.js @@ -0,0 +1,111 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { WallpaperFeed } = ChromeUtils.importESModule( + "resource://activity-stream/lib/WallpaperFeed.sys.mjs" +); + +const { actionCreators: ac, actionTypes: at } = ChromeUtils.importESModule( + "resource://activity-stream/common/Actions.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + Utils: "resource://services-settings/Utils.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +const PREF_WALLPAPERS_ENABLED = + "browser.newtabpage.activity-stream.newtabWallpapers.enabled"; + +add_task(async function test_construction() { + let feed = new WallpaperFeed(); + + info("WallpaperFeed constructor should create initial values"); + + Assert.ok(feed, "Could construct a WallpaperFeed"); + Assert.ok(feed.loaded === false, "WallpaperFeed is not loaded"); + Assert.ok( + feed.wallpaperClient === "", + "wallpaperClient is initialized as an empty string" + ); + Assert.ok( + feed.wallpaperDB === "", + "wallpaperDB is initialized as an empty string" + ); + Assert.ok( + feed.baseAttachmentURL === "", + "baseAttachmentURL is initialized as an empty string" + ); +}); + +add_task(async function test_onAction_INIT() { + let sandbox = sinon.createSandbox(); + let feed = new WallpaperFeed(); + Services.prefs.setBoolPref(PREF_WALLPAPERS_ENABLED, true); + const attachment = { + attachment: { + location: "attachment", + }, + }; + sandbox.stub(feed, "RemoteSettings").returns({ + get: () => [attachment], + on: () => {}, + }); + sandbox.stub(Utils, "SERVER_URL").returns("http://localhost:8888/v1"); + feed.store = { + dispatch: sinon.spy(), + }; + sandbox.stub(feed, "fetch").resolves({ + json: () => ({ + capabilities: { + attachments: { + base_url: "http://localhost:8888/base_url/", + }, + }, + }), + }); + + info("WallpaperFeed.onAction INIT should initialize wallpapers"); + + await feed.onAction({ + type: at.INIT, + }); + + Assert.ok(feed.store.dispatch.calledOnce); + Assert.ok( + feed.store.dispatch.calledWith( + ac.BroadcastToContent({ + type: at.WALLPAPERS_SET, + data: [ + { + ...attachment, + wallpaperUrl: "http://localhost:8888/base_url/attachment", + }, + ], + }) + ) + ); + Services.prefs.clearUserPref(PREF_WALLPAPERS_ENABLED); + sandbox.restore(); +}); + +add_task(async function test_onAction_PREF_CHANGED() { + let sandbox = sinon.createSandbox(); + let feed = new WallpaperFeed(); + Services.prefs.setBoolPref(PREF_WALLPAPERS_ENABLED, true); + sandbox.stub(feed, "wallpaperSetup").returns(); + + info("WallpaperFeed.onAction PREF_CHANGED should call wallpaperSetup"); + + feed.onAction({ + type: at.PREF_CHANGED, + data: { name: "newtabWallpapers.enabled" }, + }); + + Assert.ok(feed.wallpaperSetup.calledOnce); + + Services.prefs.clearUserPref(PREF_WALLPAPERS_ENABLED); + sandbox.restore(); +}); diff --git a/browser/components/newtab/test/xpcshell/xpcshell.toml b/browser/components/newtab/test/xpcshell/xpcshell.toml index 87d73669d35e..13c11b05416d 100644 --- a/browser/components/newtab/test/xpcshell/xpcshell.toml +++ b/browser/components/newtab/test/xpcshell/xpcshell.toml @@ -26,3 +26,5 @@ support-files = ["../schemas/*.schema.json"] ["test_TopSitesFeed.js"] ["test_TopSitesFeed_glean.js"] + +["test_WallpaperFeed.js"] diff --git a/browser/locales/en-US/browser/newtab/newtab.ftl b/browser/locales/en-US/browser/newtab/newtab.ftl index 256a8da57660..50bd34f71787 100644 --- a/browser/locales/en-US/browser/newtab/newtab.ftl +++ b/browser/locales/en-US/browser/newtab/newtab.ftl @@ -272,3 +272,25 @@ newtab-custom-recent-toggle = .description = A selection of recent sites and content newtab-custom-close-button = Close newtab-custom-settings = Manage more settings + +## New Tab Wallpapers + +newtab-wallpaper-title = Wallpapers +newtab-wallpaper-reset = Reset to default +newtab-wallpaper-light-red-panda = Red panda +newtab-wallpaper-light-mountain = White mountain +newtab-wallpaper-light-sky = Sky with purple and pink clouds +newtab-wallpaper-light-color = Blue, pink and yellow shapes +newtab-wallpaper-light-landscape = Blue mist mountain landscape +newtab-wallpaper-light-beach = Beach with palm tree +newtab-wallpaper-dark-aurora = Aurora Borealis +newtab-wallpaper-dark-color = Red and blue shapes +newtab-wallpaper-dark-panda = Red panda hidden in forest +newtab-wallpaper-dark-sky = City landscape with a night sky +newtab-wallpaper-dark-mountain = Landscape mountain +newtab-wallpaper-dark-city = Purple city landscape + +# Variables +# $author_string (String) - The name of the creator of the photo. +# $webpage_string (String) - The name of the webpage where the photo is located. +newtab-wallpaper-attribution = Photo by { $author_string } on { $webpage_string } diff --git a/toolkit/components/nimbus/FeatureManifest.yaml b/toolkit/components/nimbus/FeatureManifest.yaml index d221a0015b95..2647626852fb 100644 --- a/toolkit/components/nimbus/FeatureManifest.yaml +++ b/toolkit/components/nimbus/FeatureManifest.yaml @@ -840,6 +840,13 @@ pocketNewtab: browser.newtabpage.activity-stream.discoverystream.sendToPocket.enabled description: >- Decides what to do when a logged out user click "Save to Pocket" from a Pocket card. + wallpapers: + type: boolean + setPref: + branch: user + pref: browser.newtabpage.activity-stream.newtabWallpapers.enabled + description: >- + Turns on and off wallpaper support. recsPersonalized: type: boolean fallbackPref: >- -- 2.11.4.GIT