From def92d1b8e9d373e2f6f27c366d578d97d8960c6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 15 May 2024 05:34:50 +0200 Subject: Merging upstream version 126.0. Signed-off-by: Daniel Baumann --- .../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 +++++++++++++++++ 14 files changed, 990 insertions(+), 989 deletions(-) delete mode 100644 browser/components/newtab/content-src/lib/constants.js create mode 100644 browser/components/newtab/content-src/lib/constants.mjs delete mode 100644 browser/components/newtab/content-src/lib/detect-user-session-start.js create mode 100644 browser/components/newtab/content-src/lib/detect-user-session-start.mjs delete mode 100644 browser/components/newtab/content-src/lib/init-store.js create mode 100644 browser/components/newtab/content-src/lib/init-store.mjs delete mode 100644 browser/components/newtab/content-src/lib/link-menu-options.js create mode 100644 browser/components/newtab/content-src/lib/link-menu-options.mjs delete mode 100644 browser/components/newtab/content-src/lib/perf-service.js create mode 100644 browser/components/newtab/content-src/lib/perf-service.mjs delete mode 100644 browser/components/newtab/content-src/lib/screenshot-utils.js create mode 100644 browser/components/newtab/content-src/lib/screenshot-utils.mjs delete mode 100644 browser/components/newtab/content-src/lib/selectLayoutRender.js create mode 100644 browser/components/newtab/content-src/lib/selectLayoutRender.mjs (limited to 'browser/components/newtab/content-src/lib') 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 }; +}; -- cgit v1.2.3