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