activity-stream.bundle.js (629777B)
1 /*! THIS FILE IS AUTO-GENERATED: webpack.system-addon.config.js */ 2 var NewtabRenderUtils; 3 /******/ (() => { // webpackBootstrap 4 /******/ "use strict"; 5 /******/ // The require scope 6 /******/ var __webpack_require__ = {}; 7 /******/ 8 /************************************************************************/ 9 /******/ /* webpack/runtime/compat get default export */ 10 /******/ (() => { 11 /******/ // getDefaultExport function for compatibility with non-harmony modules 12 /******/ __webpack_require__.n = (module) => { 13 /******/ var getter = module && module.__esModule ? 14 /******/ () => (module['default']) : 15 /******/ () => (module); 16 /******/ __webpack_require__.d(getter, { a: getter }); 17 /******/ return getter; 18 /******/ }; 19 /******/ })(); 20 /******/ 21 /******/ /* webpack/runtime/define property getters */ 22 /******/ (() => { 23 /******/ // define getter functions for harmony exports 24 /******/ __webpack_require__.d = (exports, definition) => { 25 /******/ for(var key in definition) { 26 /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { 27 /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); 28 /******/ } 29 /******/ } 30 /******/ }; 31 /******/ })(); 32 /******/ 33 /******/ /* webpack/runtime/global */ 34 /******/ (() => { 35 /******/ __webpack_require__.g = (function() { 36 /******/ if (typeof globalThis === 'object') return globalThis; 37 /******/ try { 38 /******/ return this || new Function('return this')(); 39 /******/ } catch (e) { 40 /******/ if (typeof window === 'object') return window; 41 /******/ } 42 /******/ })(); 43 /******/ })(); 44 /******/ 45 /******/ /* webpack/runtime/hasOwnProperty shorthand */ 46 /******/ (() => { 47 /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) 48 /******/ })(); 49 /******/ 50 /******/ /* webpack/runtime/make namespace object */ 51 /******/ (() => { 52 /******/ // define __esModule on exports 53 /******/ __webpack_require__.r = (exports) => { 54 /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 55 /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 56 /******/ } 57 /******/ Object.defineProperty(exports, '__esModule', { value: true }); 58 /******/ }; 59 /******/ })(); 60 /******/ 61 /************************************************************************/ 62 var __webpack_exports__ = {}; 63 // ESM COMPAT FLAG 64 __webpack_require__.r(__webpack_exports__); 65 66 // EXPORTS 67 __webpack_require__.d(__webpack_exports__, { 68 NewTab: () => (/* binding */ NewTab), 69 renderCache: () => (/* binding */ renderCache), 70 renderWithoutState: () => (/* binding */ renderWithoutState) 71 }); 72 73 ;// CONCATENATED MODULE: ./common/Actions.mjs 74 /* This Source Code Form is subject to the terms of the Mozilla Public 75 * License, v. 2.0. If a copy of the MPL was not distributed with this 76 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 77 78 // This file is accessed from both content and system scopes. 79 80 const MAIN_MESSAGE_TYPE = "ActivityStream:Main"; 81 const CONTENT_MESSAGE_TYPE = "ActivityStream:Content"; 82 const PRELOAD_MESSAGE_TYPE = "ActivityStream:PreloadedBrowser"; 83 const UI_CODE = 1; 84 const BACKGROUND_PROCESS = 2; 85 86 /** 87 * globalImportContext - Are we in UI code (i.e. react, a dom) or some kind of background process? 88 * Use this in action creators if you need different logic 89 * for ui/background processes. 90 */ 91 const globalImportContext = 92 typeof Window === "undefined" ? BACKGROUND_PROCESS : UI_CODE; 93 94 // Create an object that avoids accidental differing key/value pairs: 95 // { 96 // INIT: "INIT", 97 // UNINIT: "UNINIT" 98 // } 99 const actionTypes = {}; 100 101 for (const type of [ 102 "ABOUT_SPONSORED_TOP_SITES", 103 "ADDONS_INFO_REQUEST", 104 "ADDONS_INFO_RESPONSE", 105 "ADS_FEED_UPDATE", 106 "ADS_INIT", 107 "ADS_RESET", 108 "ADS_UPDATE_SPOCS", 109 "ADS_UPDATE_TILES", 110 "BLOCK_SECTION", 111 "BLOCK_URL", 112 "BOOKMARK_URL", 113 "CARD_SECTION_IMPRESSION", 114 "CLEAR_PREF", 115 "COPY_DOWNLOAD_LINK", 116 "DELETE_BOOKMARK_BY_ID", 117 "DELETE_HISTORY_URL", 118 "DIALOG_CANCEL", 119 "DIALOG_CLOSE", 120 "DIALOG_OPEN", 121 "DISABLE_SEARCH", 122 "DISCOVERY_STREAM_CONFIG_CHANGE", 123 "DISCOVERY_STREAM_CONFIG_RESET", 124 "DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS", 125 "DISCOVERY_STREAM_CONFIG_SETUP", 126 "DISCOVERY_STREAM_CONFIG_SET_VALUE", 127 "DISCOVERY_STREAM_DEV_BLOCKS", 128 "DISCOVERY_STREAM_DEV_BLOCKS_RESET", 129 "DISCOVERY_STREAM_DEV_EXPIRE_CACHE", 130 "DISCOVERY_STREAM_DEV_IDLE_DAILY", 131 "DISCOVERY_STREAM_DEV_IMPRESSIONS", 132 "DISCOVERY_STREAM_DEV_SHOW_PLACEHOLDER", 133 "DISCOVERY_STREAM_DEV_SYNC_RS", 134 "DISCOVERY_STREAM_DEV_SYSTEM_TICK", 135 "DISCOVERY_STREAM_EXPERIMENT_DATA", 136 "DISCOVERY_STREAM_FEEDS_UPDATE", 137 "DISCOVERY_STREAM_FEED_UPDATE", 138 "DISCOVERY_STREAM_IMPRESSION_STATS", 139 "DISCOVERY_STREAM_LAYOUT_RESET", 140 "DISCOVERY_STREAM_LAYOUT_UPDATE", 141 "DISCOVERY_STREAM_LINK_BLOCKED", 142 "DISCOVERY_STREAM_LOADED_CONTENT", 143 "DISCOVERY_STREAM_PERSONALIZATION_INIT", 144 "DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED", 145 "DISCOVERY_STREAM_PERSONALIZATION_OVERRIDE", 146 "DISCOVERY_STREAM_PERSONALIZATION_RESET", 147 "DISCOVERY_STREAM_PERSONALIZATION_TOGGLE", 148 "DISCOVERY_STREAM_PERSONALIZATION_UPDATED", 149 "DISCOVERY_STREAM_PREFS_SETUP", 150 "DISCOVERY_STREAM_RETRY_FEED", 151 "DISCOVERY_STREAM_SPOCS_CAPS", 152 "DISCOVERY_STREAM_SPOCS_ENDPOINT", 153 "DISCOVERY_STREAM_SPOCS_ONDEMAND_LOAD", 154 "DISCOVERY_STREAM_SPOCS_ONDEMAND_RESET", 155 "DISCOVERY_STREAM_SPOCS_ONDEMAND_UPDATE", 156 "DISCOVERY_STREAM_SPOCS_PLACEMENTS", 157 "DISCOVERY_STREAM_SPOCS_UPDATE", 158 "DISCOVERY_STREAM_SPOC_BLOCKED", 159 "DISCOVERY_STREAM_SPOC_IMPRESSION", 160 "DISCOVERY_STREAM_SPOC_PLACEHOLDER_DURATION", 161 "DISCOVERY_STREAM_TOPICS_LOADING", 162 "DISCOVERY_STREAM_USER_EVENT", 163 "DOWNLOAD_CHANGED", 164 "FAKE_FOCUS_SEARCH", 165 "FILL_SEARCH_TERM", 166 "FOLLOW_SECTION", 167 "HANDOFF_SEARCH_TO_AWESOMEBAR", 168 "HIDE_PERSONALIZE", 169 "HIDE_TOAST_MESSAGE", 170 "INFERRED_PERSONALIZATION_MODEL_UPDATE", 171 "INFERRED_PERSONALIZATION_REFRESH", 172 "INFERRED_PERSONALIZATION_RESET", 173 "INFERRED_PERSONALIZATION_UPDATE", 174 "INIT", 175 "INLINE_SELECTION_CLICK", 176 "INLINE_SELECTION_IMPRESSION", 177 "MESSAGE_BLOCK", 178 "MESSAGE_CLICK", 179 "MESSAGE_DISMISS", 180 "MESSAGE_IMPRESSION", 181 "MESSAGE_NOTIFY_VISIBILITY", 182 "MESSAGE_SET", 183 "MESSAGE_TOGGLE_VISIBILITY", 184 "NEW_TAB_INIT", 185 "NEW_TAB_INITIAL_STATE", 186 "NEW_TAB_LOAD", 187 "NEW_TAB_REHYDRATED", 188 "NEW_TAB_STATE_REQUEST", 189 "NEW_TAB_STATE_REQUEST_STARTUPCACHE", 190 "NEW_TAB_STATE_REQUEST_WITHOUT_STARTUPCACHE", 191 "NEW_TAB_UNLOAD", 192 "OPEN_DOWNLOAD_FILE", 193 "OPEN_LINK", 194 "OPEN_NEW_WINDOW", 195 "OPEN_PRIVATE_WINDOW", 196 "OPEN_WEBEXT_SETTINGS", 197 "PARTNER_LINK_ATTRIBUTION", 198 "PLACES_BOOKMARKS_REMOVED", 199 "PLACES_BOOKMARK_ADDED", 200 "PLACES_HISTORY_CLEARED", 201 "PLACES_LINKS_CHANGED", 202 "PLACES_LINKS_DELETED", 203 "PLACES_LINK_BLOCKED", 204 "POCKET_CTA", 205 "POCKET_WAITING_FOR_SPOC", 206 "PREFS_INITIAL_VALUES", 207 "PREF_CHANGED", 208 "PREVIEW_REQUEST", 209 "PREVIEW_REQUEST_CANCEL", 210 "PREVIEW_RESPONSE", 211 "PROMO_CARD_CLICK", 212 "PROMO_CARD_DISMISS", 213 "PROMO_CARD_IMPRESSION", 214 "REFRESH_EXTERNAL_COMPONENTS", 215 "REMOVE_DOWNLOAD_FILE", 216 "REPORT_AD_OPEN", 217 "REPORT_AD_SUBMIT", 218 "REPORT_CLOSE", 219 "REPORT_CONTENT_OPEN", 220 "REPORT_CONTENT_SUBMIT", 221 "RICH_ICON_MISSING", 222 "SAVE_SESSION_PERF_DATA", 223 "SCREENSHOT_UPDATED", 224 "SECTION_DEREGISTER", 225 "SECTION_DISABLE", 226 "SECTION_ENABLE", 227 "SECTION_OPTIONS_CHANGED", 228 "SECTION_PERSONALIZATION_SET", 229 "SECTION_PERSONALIZATION_UPDATE", 230 "SECTION_REGISTER", 231 "SECTION_UPDATE", 232 "SECTION_UPDATE_CARD", 233 "SETTINGS_CLOSE", 234 "SETTINGS_OPEN", 235 "SET_PREF", 236 "SHOW_DOWNLOAD_FILE", 237 "SHOW_FIREFOX_ACCOUNTS", 238 "SHOW_PERSONALIZE", 239 "SHOW_PRIVACY_INFO", 240 "SHOW_SEARCH", 241 "SHOW_TOAST_MESSAGE", 242 "SKIPPED_SIGNIN", 243 "SOV_UPDATED", 244 "SUBMIT_EMAIL", 245 "SUBMIT_SIGNIN", 246 "SYSTEM_TICK", 247 "TELEMETRY_IMPRESSION_STATS", 248 "TELEMETRY_USER_EVENT", 249 "TOPIC_SELECTION_IMPRESSION", 250 "TOPIC_SELECTION_MAYBE_LATER", 251 "TOPIC_SELECTION_SPOTLIGHT_CLOSE", 252 "TOPIC_SELECTION_SPOTLIGHT_OPEN", 253 "TOPIC_SELECTION_USER_DISMISS", 254 "TOPIC_SELECTION_USER_OPEN", 255 "TOPIC_SELECTION_USER_SAVE", 256 "TOP_SITES_ADD", 257 "TOP_SITES_CANCEL_EDIT", 258 "TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL", 259 "TOP_SITES_EDIT", 260 "TOP_SITES_INSERT", 261 "TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL", 262 "TOP_SITES_ORGANIC_IMPRESSION_STATS", 263 "TOP_SITES_PIN", 264 "TOP_SITES_PREFS_UPDATED", 265 "TOP_SITES_SPONSORED_IMPRESSION_STATS", 266 "TOP_SITES_UNPIN", 267 "TOP_SITES_UPDATED", 268 "TOTAL_BOOKMARKS_REQUEST", 269 "TOTAL_BOOKMARKS_RESPONSE", 270 "UNBLOCK_SECTION", 271 "UNFOLLOW_SECTION", 272 "UNINIT", 273 "UPDATE_PINNED_SEARCH_SHORTCUTS", 274 "UPDATE_SEARCH_SHORTCUTS", 275 "WALLPAPERS_CATEGORY_SET", 276 "WALLPAPERS_CUSTOM_SET", 277 "WALLPAPERS_FEATURE_HIGHLIGHT_COUNTER_INCREMENT", 278 "WALLPAPERS_FEATURE_HIGHLIGHT_CTA_CLICKED", 279 "WALLPAPERS_FEATURE_HIGHLIGHT_DISMISSED", 280 "WALLPAPERS_FEATURE_HIGHLIGHT_SEEN", 281 "WALLPAPERS_SET", 282 "WALLPAPER_CATEGORY_CLICK", 283 "WALLPAPER_CLICK", 284 "WALLPAPER_REMOVE_UPLOAD", 285 "WALLPAPER_UPLOAD", 286 "WEATHER_DETECT_LOCATION", 287 "WEATHER_IMPRESSION", 288 "WEATHER_LOAD_ERROR", 289 "WEATHER_LOCATION_DATA_UPDATE", 290 "WEATHER_LOCATION_SEARCH_UPDATE", 291 "WEATHER_LOCATION_SUGGESTIONS_UPDATE", 292 "WEATHER_OPEN_PROVIDER_URL", 293 "WEATHER_OPT_IN_PROMPT_SELECTION", 294 "WEATHER_QUERY_UPDATE", 295 "WEATHER_SEARCH_ACTIVE", 296 "WEATHER_UPDATE", 297 "WEATHER_USER_OPT_IN_LOCATION", 298 "WEBEXT_CLICK", 299 "WEBEXT_DISMISS", 300 "WIDGETS_LISTS_CHANGE_SELECTED", 301 "WIDGETS_LISTS_SET", 302 "WIDGETS_LISTS_SET_SELECTED", 303 "WIDGETS_LISTS_UPDATE", 304 "WIDGETS_LISTS_USER_EVENT", 305 "WIDGETS_LISTS_USER_IMPRESSION", 306 "WIDGETS_TIMER_END", 307 "WIDGETS_TIMER_PAUSE", 308 "WIDGETS_TIMER_PLAY", 309 "WIDGETS_TIMER_RESET", 310 "WIDGETS_TIMER_SET", 311 "WIDGETS_TIMER_SET_DURATION", 312 "WIDGETS_TIMER_SET_TYPE", 313 "WIDGETS_TIMER_USER_EVENT", 314 "WIDGETS_TIMER_USER_IMPRESSION", 315 ]) { 316 actionTypes[type] = type; 317 } 318 319 // Helper function for creating routed actions between content and main 320 // Not intended to be used by consumers 321 function _RouteMessage(action, options) { 322 const meta = action.meta ? { ...action.meta } : {}; 323 if (!options || !options.from || !options.to) { 324 throw new Error( 325 "Routed Messages must have options as the second parameter, and must at least include a .from and .to property." 326 ); 327 } 328 // For each of these fields, if they are passed as an option, 329 // add them to the action. If they are not defined, remove them. 330 ["from", "to", "toTarget", "fromTarget", "skipMain", "skipLocal"].forEach( 331 o => { 332 if (typeof options[o] !== "undefined") { 333 meta[o] = options[o]; 334 } else if (meta[o]) { 335 delete meta[o]; 336 } 337 } 338 ); 339 return { ...action, meta }; 340 } 341 342 /** 343 * AlsoToMain - Creates a message that will be dispatched locally and also sent to the Main process. 344 * 345 * @param {object} action Any redux action (required) 346 * @param {object} options 347 * @param {bool} skipLocal Used by OnlyToMain to skip the main reducer 348 * @param {string} fromTarget The id of the content port from which the action originated. (optional) 349 * @return {object} An action with added .meta properties 350 */ 351 function AlsoToMain(action, fromTarget, skipLocal) { 352 return _RouteMessage(action, { 353 from: CONTENT_MESSAGE_TYPE, 354 to: MAIN_MESSAGE_TYPE, 355 fromTarget, 356 skipLocal, 357 }); 358 } 359 360 /** 361 * OnlyToMain - Creates a message that will be sent to the Main process and skip the local reducer. 362 * 363 * @param {object} action Any redux action (required) 364 * @param {object} options 365 * @param {string} fromTarget The id of the content port from which the action originated. (optional) 366 * @return {object} An action with added .meta properties 367 */ 368 function OnlyToMain(action, fromTarget) { 369 return AlsoToMain(action, fromTarget, true); 370 } 371 372 /** 373 * BroadcastToContent - Creates a message that will be dispatched to main and sent to ALL content processes. 374 * 375 * @param {object} action Any redux action (required) 376 * @param {object} options (optional) 377 * @return {object} An action with added .meta properties 378 */ 379 function BroadcastToContent(action, options) { 380 return _RouteMessage(action, { 381 from: MAIN_MESSAGE_TYPE, 382 to: CONTENT_MESSAGE_TYPE, 383 ...options, 384 }); 385 } 386 387 /** 388 * AlsoToOneContent - Creates a message that will be will be dispatched to the main store 389 * and also sent to a particular Content process. 390 * 391 * @param {object} action Any redux action (required) 392 * @param {string} target The id of a content port 393 * @param {bool} skipMain Used by OnlyToOneContent to skip the main process 394 * @return {object} An action with added .meta properties 395 */ 396 function AlsoToOneContent(action, target, skipMain) { 397 if (!target) { 398 throw new Error( 399 "You must provide a target ID as the second parameter of AlsoToOneContent. If you want to send to all content processes, use BroadcastToContent" 400 ); 401 } 402 return _RouteMessage(action, { 403 from: MAIN_MESSAGE_TYPE, 404 to: CONTENT_MESSAGE_TYPE, 405 toTarget: target, 406 skipMain, 407 }); 408 } 409 410 /** 411 * OnlyToOneContent - Creates a message that will be sent to a particular Content process 412 * and skip the main reducer. 413 * 414 * @param {object} action Any redux action (required) 415 * @param {string} target The id of a content port 416 * @return {object} An action with added .meta properties 417 */ 418 function OnlyToOneContent(action, target) { 419 return AlsoToOneContent(action, target, true); 420 } 421 422 /** 423 * AlsoToPreloaded - Creates a message that dispatched to the main reducer and also sent to the preloaded tab. 424 * 425 * @param {object} action Any redux action (required) 426 * @return {object} An action with added .meta properties 427 */ 428 function AlsoToPreloaded(action) { 429 return _RouteMessage(action, { 430 from: MAIN_MESSAGE_TYPE, 431 to: PRELOAD_MESSAGE_TYPE, 432 }); 433 } 434 435 /** 436 * UserEvent - A telemetry ping indicating a user action. This should only 437 * be sent from the UI during a user session. 438 * 439 * @param {object} data Fields to include in the ping (source, etc.) 440 * @return {object} An AlsoToMain action 441 */ 442 function UserEvent(data) { 443 return AlsoToMain({ 444 type: actionTypes.TELEMETRY_USER_EVENT, 445 data, 446 }); 447 } 448 449 /** 450 * DiscoveryStreamUserEvent - A telemetry ping indicating a user action from Discovery Stream. This should only 451 * be sent from the UI during a user session. 452 * 453 * @param {object} data Fields to include in the ping (source, etc.) 454 * @return {object} An AlsoToMain action 455 */ 456 function DiscoveryStreamUserEvent(data) { 457 return AlsoToMain({ 458 type: actionTypes.DISCOVERY_STREAM_USER_EVENT, 459 data, 460 }); 461 } 462 463 /** 464 * ImpressionStats - A telemetry ping indicating an impression stats. 465 * 466 * @param {object} data Fields to include in the ping 467 * @param {int} importContext (For testing) Override the import context for testing. 468 * #return {object} An action. For UI code, a AlsoToMain action. 469 */ 470 function ImpressionStats(data, importContext = globalImportContext) { 471 const action = { 472 type: actionTypes.TELEMETRY_IMPRESSION_STATS, 473 data, 474 }; 475 return importContext === UI_CODE ? AlsoToMain(action) : action; 476 } 477 478 /** 479 * DiscoveryStreamImpressionStats - A telemetry ping indicating an impression stats in Discovery Stream. 480 * 481 * @param {object} data Fields to include in the ping 482 * @param {int} importContext (For testing) Override the import context for testing. 483 * #return {object} An action. For UI code, a AlsoToMain action. 484 */ 485 function DiscoveryStreamImpressionStats( 486 data, 487 importContext = globalImportContext 488 ) { 489 const action = { 490 type: actionTypes.DISCOVERY_STREAM_IMPRESSION_STATS, 491 data, 492 }; 493 return importContext === UI_CODE ? AlsoToMain(action) : action; 494 } 495 496 /** 497 * DiscoveryStreamLoadedContent - A telemetry ping indicating a content gets loaded in Discovery Stream. 498 * 499 * @param {object} data Fields to include in the ping 500 * @param {int} importContext (For testing) Override the import context for testing. 501 * #return {object} An action. For UI code, a AlsoToMain action. 502 */ 503 function DiscoveryStreamLoadedContent( 504 data, 505 importContext = globalImportContext 506 ) { 507 const action = { 508 type: actionTypes.DISCOVERY_STREAM_LOADED_CONTENT, 509 data, 510 }; 511 return importContext === UI_CODE ? AlsoToMain(action) : action; 512 } 513 514 function SetPref(prefName, value, importContext = globalImportContext) { 515 const action = { 516 type: actionTypes.SET_PREF, 517 data: { name: prefName, value }, 518 }; 519 return importContext === UI_CODE ? AlsoToMain(action) : action; 520 } 521 522 function WebExtEvent(type, data, importContext = globalImportContext) { 523 if (!data || !data.source) { 524 throw new Error( 525 'WebExtEvent actions should include a property "source", the id of the webextension that should receive the event.' 526 ); 527 } 528 const action = { type, data }; 529 return importContext === UI_CODE ? AlsoToMain(action) : action; 530 } 531 532 const actionCreators = { 533 BroadcastToContent, 534 UserEvent, 535 DiscoveryStreamUserEvent, 536 ImpressionStats, 537 AlsoToOneContent, 538 OnlyToOneContent, 539 AlsoToMain, 540 OnlyToMain, 541 AlsoToPreloaded, 542 SetPref, 543 WebExtEvent, 544 DiscoveryStreamImpressionStats, 545 DiscoveryStreamLoadedContent, 546 }; 547 548 // These are helpers to test for certain kinds of actions 549 const actionUtils = { 550 isSendToMain(action) { 551 if (!action.meta) { 552 return false; 553 } 554 return ( 555 action.meta.to === MAIN_MESSAGE_TYPE && 556 action.meta.from === CONTENT_MESSAGE_TYPE 557 ); 558 }, 559 isBroadcastToContent(action) { 560 if (!action.meta) { 561 return false; 562 } 563 if (action.meta.to === CONTENT_MESSAGE_TYPE && !action.meta.toTarget) { 564 return true; 565 } 566 return false; 567 }, 568 isSendToOneContent(action) { 569 if (!action.meta) { 570 return false; 571 } 572 if (action.meta.to === CONTENT_MESSAGE_TYPE && action.meta.toTarget) { 573 return true; 574 } 575 return false; 576 }, 577 isSendToPreloaded(action) { 578 if (!action.meta) { 579 return false; 580 } 581 return ( 582 action.meta.to === PRELOAD_MESSAGE_TYPE && 583 action.meta.from === MAIN_MESSAGE_TYPE 584 ); 585 }, 586 isFromMain(action) { 587 if (!action.meta) { 588 return false; 589 } 590 return ( 591 action.meta.from === MAIN_MESSAGE_TYPE && 592 action.meta.to === CONTENT_MESSAGE_TYPE 593 ); 594 }, 595 getPortIdOfSender(action) { 596 return (action.meta && action.meta.fromTarget) || null; 597 }, 598 _RouteMessage, 599 }; 600 601 ;// CONCATENATED MODULE: external "ReactRedux" 602 const external_ReactRedux_namespaceObject = ReactRedux; 603 ;// CONCATENATED MODULE: external "React" 604 const external_React_namespaceObject = React; 605 var external_React_default = /*#__PURE__*/__webpack_require__.n(external_React_namespaceObject); 606 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx 607 function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } 608 /* This Source Code Form is subject to the terms of the Mozilla Public 609 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 610 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 611 612 613 614 615 616 // Pref Constants 617 const PREF_AD_SIZE_MEDIUM_RECTANGLE = "newtabAdSize.mediumRectangle"; 618 const PREF_AD_SIZE_BILLBOARD = "newtabAdSize.billboard"; 619 const PREF_AD_SIZE_LEADERBOARD = "newtabAdSize.leaderboard"; 620 const PREF_SECTIONS_ENABLED = "discoverystream.sections.enabled"; 621 const PREF_SPOC_PLACEMENTS = "discoverystream.placements.spocs"; 622 const PREF_SPOC_COUNTS = "discoverystream.placements.spocs.counts"; 623 const PREF_CONTEXTUAL_ADS_ENABLED = "discoverystream.sections.contextualAds.enabled"; 624 const PREF_CONTEXTUAL_BANNER_PLACEMENTS = "discoverystream.placements.contextualBanners"; 625 const PREF_CONTEXTUAL_BANNER_COUNTS = "discoverystream.placements.contextualBanners.counts"; 626 const PREF_UNIFIED_ADS_ENABLED = "unifiedAds.spocs.enabled"; 627 const PREF_UNIFIED_ADS_ENDPOINT = "unifiedAds.endpoint"; 628 const PREF_ALLOWED_ENDPOINTS = "discoverystream.endpoints"; 629 const PREF_OHTTP_CONFIG = "discoverystream.ohttp.configURL"; 630 const PREF_OHTTP_RELAY = "discoverystream.ohttp.relayURL"; 631 const Row = props => /*#__PURE__*/external_React_default().createElement("tr", _extends({ 632 className: "message-item" 633 }, props), props.children); 634 function relativeTime(timestamp) { 635 if (!timestamp) { 636 return ""; 637 } 638 const seconds = Math.floor((Date.now() - timestamp) / 1000); 639 const minutes = Math.floor((Date.now() - timestamp) / 60000); 640 if (seconds < 2) { 641 return "just now"; 642 } else if (seconds < 60) { 643 return `${seconds} seconds ago`; 644 } else if (minutes === 1) { 645 return "1 minute ago"; 646 } else if (minutes < 600) { 647 return `${minutes} minutes ago`; 648 } 649 return new Date(timestamp).toLocaleString(); 650 } 651 class ToggleStoryButton extends (external_React_default()).PureComponent { 652 constructor(props) { 653 super(props); 654 this.handleClick = this.handleClick.bind(this); 655 } 656 handleClick() { 657 this.props.onClick(this.props.story); 658 } 659 render() { 660 return /*#__PURE__*/external_React_default().createElement("button", { 661 onClick: this.handleClick 662 }, "collapse/open"); 663 } 664 } 665 class TogglePrefCheckbox extends (external_React_default()).PureComponent { 666 constructor(props) { 667 super(props); 668 this.onChange = this.onChange.bind(this); 669 } 670 onChange(event) { 671 this.props.onChange(this.props.pref, event.target.checked); 672 } 673 render() { 674 return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("input", { 675 type: "checkbox", 676 checked: this.props.checked, 677 onChange: this.onChange, 678 disabled: this.props.disabled 679 }), " ", this.props.pref, " "); 680 } 681 } 682 class Personalization extends (external_React_default()).PureComponent { 683 constructor(props) { 684 super(props); 685 this.togglePersonalization = this.togglePersonalization.bind(this); 686 } 687 togglePersonalization() { 688 this.props.dispatch(actionCreators.OnlyToMain({ 689 type: actionTypes.DISCOVERY_STREAM_PERSONALIZATION_TOGGLE 690 })); 691 } 692 render() { 693 const { 694 lastUpdated, 695 initialized 696 } = this.props.state.Personalization; 697 return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", { 698 colSpan: "2" 699 }, /*#__PURE__*/external_React_default().createElement(TogglePrefCheckbox, { 700 checked: this.props.personalized, 701 pref: "personalized", 702 onChange: this.togglePersonalization 703 }))), /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", { 704 className: "min" 705 }, "Personalization Last Updated"), /*#__PURE__*/external_React_default().createElement("td", null, relativeTime(lastUpdated) || "(no data)")), /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", { 706 className: "min" 707 }, "Personalization Initialized"), /*#__PURE__*/external_React_default().createElement("td", null, initialized ? "true" : "false"))))); 708 } 709 } 710 class DiscoveryStreamAdminUI extends (external_React_default()).PureComponent { 711 constructor(props) { 712 super(props); 713 this.restorePrefDefaults = this.restorePrefDefaults.bind(this); 714 this.setConfigValue = this.setConfigValue.bind(this); 715 this.expireCache = this.expireCache.bind(this); 716 this.refreshCache = this.refreshCache.bind(this); 717 this.showPlaceholder = this.showPlaceholder.bind(this); 718 this.idleDaily = this.idleDaily.bind(this); 719 this.systemTick = this.systemTick.bind(this); 720 this.syncRemoteSettings = this.syncRemoteSettings.bind(this); 721 this.onStoryToggle = this.onStoryToggle.bind(this); 722 this.handleWeatherSubmit = this.handleWeatherSubmit.bind(this); 723 this.handleWeatherUpdate = this.handleWeatherUpdate.bind(this); 724 this.resetBlocks = this.resetBlocks.bind(this); 725 this.refreshInferredPersonalization = this.refreshInferredPersonalization.bind(this); 726 this.refreshTopicSelectionCache = this.refreshTopicSelectionCache.bind(this); 727 this.handleSectionsToggle = this.handleSectionsToggle.bind(this); 728 this.toggleIABBanners = this.toggleIABBanners.bind(this); 729 this.handleAllizomToggle = this.handleAllizomToggle.bind(this); 730 this.sendConversionEvent = this.sendConversionEvent.bind(this); 731 this.state = { 732 toggledStories: {}, 733 weatherQuery: "" 734 }; 735 } 736 setConfigValue(configName, configValue) { 737 this.props.dispatch(actionCreators.OnlyToMain({ 738 type: actionTypes.DISCOVERY_STREAM_CONFIG_SET_VALUE, 739 data: { 740 name: configName, 741 value: configValue 742 } 743 })); 744 } 745 restorePrefDefaults() { 746 this.props.dispatch(actionCreators.OnlyToMain({ 747 type: actionTypes.DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS 748 })); 749 } 750 refreshCache() { 751 const { 752 config 753 } = this.props.state.DiscoveryStream; 754 this.props.dispatch(actionCreators.OnlyToMain({ 755 type: actionTypes.DISCOVERY_STREAM_CONFIG_CHANGE, 756 data: config 757 })); 758 } 759 refreshInferredPersonalization() { 760 this.props.dispatch(actionCreators.OnlyToMain({ 761 type: actionTypes.INFERRED_PERSONALIZATION_REFRESH 762 })); 763 } 764 refreshTopicSelectionCache() { 765 this.props.dispatch(actionCreators.SetPref("discoverystream.topicSelection.onboarding.displayCount", 0)); 766 this.props.dispatch(actionCreators.SetPref("discoverystream.topicSelection.onboarding.maybeDisplay", true)); 767 } 768 dispatchSimpleAction(type) { 769 this.props.dispatch(actionCreators.OnlyToMain({ 770 type 771 })); 772 } 773 resetBlocks() { 774 this.props.dispatch(actionCreators.OnlyToMain({ 775 type: actionTypes.DISCOVERY_STREAM_DEV_BLOCKS_RESET 776 })); 777 } 778 systemTick() { 779 this.dispatchSimpleAction(actionTypes.DISCOVERY_STREAM_DEV_SYSTEM_TICK); 780 } 781 expireCache() { 782 this.dispatchSimpleAction(actionTypes.DISCOVERY_STREAM_DEV_EXPIRE_CACHE); 783 } 784 showPlaceholder() { 785 this.dispatchSimpleAction(actionTypes.DISCOVERY_STREAM_DEV_SHOW_PLACEHOLDER); 786 } 787 idleDaily() { 788 this.dispatchSimpleAction(actionTypes.DISCOVERY_STREAM_DEV_IDLE_DAILY); 789 } 790 syncRemoteSettings() { 791 this.dispatchSimpleAction(actionTypes.DISCOVERY_STREAM_DEV_SYNC_RS); 792 } 793 handleWeatherUpdate(e) { 794 this.setState({ 795 weatherQuery: e.target.value || "" 796 }); 797 } 798 handleWeatherSubmit(e) { 799 e.preventDefault(); 800 const { 801 weatherQuery 802 } = this.state; 803 this.props.dispatch(actionCreators.SetPref("weather.query", weatherQuery)); 804 } 805 toggleIABBanners(e) { 806 const { 807 pressed, 808 id 809 } = e.target; 810 811 // Set the active pref to true/false 812 switch (id) { 813 case "newtab_billboard": 814 // Update boolean pref for billboard ad size 815 this.props.dispatch(actionCreators.SetPref(PREF_AD_SIZE_BILLBOARD, pressed)); 816 break; 817 case "newtab_leaderboard": 818 // Update boolean pref for billboard ad size 819 this.props.dispatch(actionCreators.SetPref(PREF_AD_SIZE_LEADERBOARD, pressed)); 820 break; 821 case "newtab_rectangle": 822 // Update boolean pref for mediumRectangle (MREC) ad size 823 this.props.dispatch(actionCreators.SetPref(PREF_AD_SIZE_MEDIUM_RECTANGLE, pressed)); 824 break; 825 } 826 827 // Note: The counts array is passively updated whenever the placements array is updated. 828 // The default pref values for each are: 829 // PREF_SPOC_PLACEMENTS: "newtab_spocs" 830 // PREF_SPOC_COUNTS: "6" 831 const generateSpocPrefValues = () => { 832 const placements = this.props.otherPrefs[PREF_SPOC_PLACEMENTS]?.split(",").map(item => item.trim()).filter(item => item) || []; 833 const counts = this.props.otherPrefs[PREF_SPOC_COUNTS]?.split(",").map(item => item.trim()).filter(item => item) || []; 834 835 // Confirm that the IAB type will have a count value of "1" 836 const supportIABAdTypes = ["newtab_leaderboard", "newtab_rectangle", "newtab_billboard"]; 837 let countValue; 838 if (supportIABAdTypes.includes(id)) { 839 countValue = "1"; // Default count value for all IAB ad types 840 } else { 841 throw new Error("IAB ad type not supported"); 842 } 843 if (pressed) { 844 // If pressed is true, add the id to the placements array 845 if (!placements.includes(id)) { 846 placements.push(id); 847 counts.push(countValue); 848 } 849 } else { 850 // If pressed is false, remove the id from the placements array 851 const index = placements.indexOf(id); 852 if (index !== -1) { 853 placements.splice(index, 1); 854 counts.splice(index, 1); 855 } 856 } 857 return { 858 placements: placements.join(", "), 859 counts: counts.join(", ") 860 }; 861 }; 862 const { 863 placements, 864 counts 865 } = generateSpocPrefValues(); 866 867 // Update prefs with new values 868 this.props.dispatch(actionCreators.SetPref(PREF_SPOC_PLACEMENTS, placements)); 869 this.props.dispatch(actionCreators.SetPref(PREF_SPOC_COUNTS, counts)); 870 871 // If contextual ads, sections, and one of the banners are enabled 872 // update the contextualBanner prefs to include the banner value and count 873 // Else, clear the prefs 874 if (PREF_CONTEXTUAL_ADS_ENABLED && PREF_SECTIONS_ENABLED) { 875 if (PREF_AD_SIZE_BILLBOARD && placements.includes("newtab_billboard")) { 876 this.props.dispatch(actionCreators.SetPref(PREF_CONTEXTUAL_BANNER_PLACEMENTS, "newtab_billboard")); 877 this.props.dispatch(actionCreators.SetPref(PREF_CONTEXTUAL_BANNER_COUNTS, "1")); 878 } else if (PREF_AD_SIZE_LEADERBOARD && placements.includes("newtab_leaderboard")) { 879 this.props.dispatch(actionCreators.SetPref(PREF_CONTEXTUAL_BANNER_PLACEMENTS, "newtab_leaderboard")); 880 this.props.dispatch(actionCreators.SetPref(PREF_CONTEXTUAL_BANNER_COUNTS, "1")); 881 } else { 882 this.props.dispatch(actionCreators.SetPref(PREF_CONTEXTUAL_BANNER_PLACEMENTS, "")); 883 this.props.dispatch(actionCreators.SetPref(PREF_CONTEXTUAL_BANNER_COUNTS, "")); 884 } 885 } 886 } 887 handleSectionsToggle(e) { 888 const { 889 pressed 890 } = e.target; 891 this.props.dispatch(actionCreators.SetPref(PREF_SECTIONS_ENABLED, pressed)); 892 this.props.dispatch(actionCreators.SetPref("discoverystream.sections.cards.enabled", pressed)); 893 } 894 sendConversionEvent() { 895 const detail = { 896 partnerId: "295BEEF7-1E3B-4128-B8F8-858E12AA660B", 897 lookbackDays: 7, 898 impressionType: "default" 899 }; 900 const event = new CustomEvent("FirefoxConversionNotification", { 901 detail, 902 bubbles: true, 903 composed: true 904 }); 905 window?.dispatchEvent(event); 906 } 907 renderComponent(width, component) { 908 return /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", { 909 className: "min" 910 }, "Type"), /*#__PURE__*/external_React_default().createElement("td", null, component.type)), /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", { 911 className: "min" 912 }, "Width"), /*#__PURE__*/external_React_default().createElement("td", null, width)), component.feed && this.renderFeed(component.feed))); 913 } 914 renderWeatherData() { 915 const { 916 suggestions 917 } = this.props.state.Weather; 918 let weatherTable; 919 if (suggestions) { 920 weatherTable = /*#__PURE__*/external_React_default().createElement("div", { 921 className: "weather-section" 922 }, /*#__PURE__*/external_React_default().createElement("form", { 923 onSubmit: this.handleWeatherSubmit 924 }, /*#__PURE__*/external_React_default().createElement("label", { 925 htmlFor: "weather-query" 926 }, "Weather query"), /*#__PURE__*/external_React_default().createElement("input", { 927 type: "text", 928 min: "3", 929 max: "10", 930 id: "weather-query", 931 onChange: this.handleWeatherUpdate, 932 value: this.weatherQuery 933 }), /*#__PURE__*/external_React_default().createElement("button", { 934 type: "submit" 935 }, "Submit")), /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, suggestions.map(suggestion => /*#__PURE__*/external_React_default().createElement("tr", { 936 className: "message-item", 937 key: suggestion.city_name 938 }, /*#__PURE__*/external_React_default().createElement("td", { 939 className: "message-id" 940 }, /*#__PURE__*/external_React_default().createElement("span", null, suggestion.city_name, " ", /*#__PURE__*/external_React_default().createElement("br", null))), /*#__PURE__*/external_React_default().createElement("td", { 941 className: "message-summary" 942 }, /*#__PURE__*/external_React_default().createElement("pre", null, JSON.stringify(suggestion, null, 2)))))))); 943 } 944 return weatherTable; 945 } 946 renderPersonalizationData() { 947 const { 948 inferredInterests, 949 coarseInferredInterests, 950 coarsePrivateInferredInterests 951 } = this.props.state.InferredPersonalization; 952 return /*#__PURE__*/external_React_default().createElement("div", null, " ", "Inferred Interests:", /*#__PURE__*/external_React_default().createElement("pre", null, JSON.stringify(inferredInterests, null, 2)), " Coarse Inferred Interests:", /*#__PURE__*/external_React_default().createElement("pre", null, JSON.stringify(coarseInferredInterests, null, 2)), " Coarse Inferred Interests With Differential Privacy:", /*#__PURE__*/external_React_default().createElement("pre", null, JSON.stringify(coarsePrivateInferredInterests, null, 2))); 953 } 954 renderFeedData(url) { 955 const { 956 feeds 957 } = this.props.state.DiscoveryStream; 958 const feed = feeds.data[url].data; 959 return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("h4", null, "Feed url: ", url), /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, feed.recommendations?.map(story => this.renderStoryData(story))))); 960 } 961 renderFeedsData() { 962 const { 963 feeds 964 } = this.props.state.DiscoveryStream; 965 return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, Object.keys(feeds.data).map(url => this.renderFeedData(url))); 966 } 967 renderImpressionsData() { 968 const { 969 impressions 970 } = this.props.state.DiscoveryStream; 971 return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("h4", null, "Feed Impressions"), /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, Object.keys(impressions.feed).map(key => { 972 return /*#__PURE__*/external_React_default().createElement(Row, { 973 key: key 974 }, /*#__PURE__*/external_React_default().createElement("td", { 975 className: "min" 976 }, key), /*#__PURE__*/external_React_default().createElement("td", null, relativeTime(impressions.feed[key]) || "(no data)")); 977 })))); 978 } 979 renderBlocksData() { 980 const { 981 blocks 982 } = this.props.state.DiscoveryStream; 983 return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("h4", null, "Blocks"), /*#__PURE__*/external_React_default().createElement("button", { 984 className: "button", 985 onClick: this.resetBlocks 986 }, "Reset Blocks"), " ", /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, Object.keys(blocks).map(key => { 987 return /*#__PURE__*/external_React_default().createElement(Row, { 988 key: key 989 }, /*#__PURE__*/external_React_default().createElement("td", { 990 className: "min" 991 }, key)); 992 })))); 993 } 994 handleAllizomToggle(e) { 995 const prefs = this.props.otherPrefs; 996 const unifiedAdsSpocsEnabled = prefs[PREF_UNIFIED_ADS_ENABLED]; 997 if (!unifiedAdsSpocsEnabled) { 998 return; 999 } 1000 const { 1001 pressed 1002 } = e.target; 1003 const { 1004 dispatch 1005 } = this.props; 1006 const allowedEndpoints = prefs[PREF_ALLOWED_ENDPOINTS]; 1007 const setPref = (pref = "", value = "") => { 1008 dispatch(actionCreators.SetPref(pref, value)); 1009 }; 1010 const clearPref = (pref = "") => { 1011 dispatch(actionCreators.OnlyToMain({ 1012 type: actionTypes.CLEAR_PREF, 1013 data: { 1014 name: pref 1015 } 1016 })); 1017 }; 1018 if (pressed) { 1019 setPref(PREF_UNIFIED_ADS_ENDPOINT, "https://ads.allizom.org/"); 1020 setPref(PREF_ALLOWED_ENDPOINTS, `${allowedEndpoints},https://ads.allizom.org/`); 1021 setPref(PREF_OHTTP_CONFIG, "https://stage.ohttp-gateway.nonprod.webservices.mozgcp.net/ohttp-configs"); 1022 setPref(PREF_OHTTP_RELAY, "https://mozilla-ohttp-relay-test.edgecompute.app/"); 1023 } else { 1024 clearPref(PREF_UNIFIED_ADS_ENDPOINT); 1025 clearPref(PREF_ALLOWED_ENDPOINTS); 1026 clearPref(PREF_OHTTP_CONFIG); 1027 clearPref(PREF_OHTTP_RELAY); 1028 } 1029 } 1030 renderSpocs() { 1031 const { 1032 spocs 1033 } = this.props.state.DiscoveryStream; 1034 const unifiedAdsSpocsEnabled = this.props.otherPrefs[PREF_UNIFIED_ADS_ENABLED]; 1035 1036 // Determine which mechanism is querying the UAPI ads server 1037 const PREF_UNIFIED_ADS_ADSFEED_ENABLED = "unifiedAds.adsFeed.enabled"; 1038 const adsFeedEnabled = this.props.otherPrefs[PREF_UNIFIED_ADS_ADSFEED_ENABLED]; 1039 const unifiedAdsEndpoint = this.props.otherPrefs[PREF_UNIFIED_ADS_ENDPOINT]; 1040 const spocsEndpoint = unifiedAdsSpocsEnabled ? unifiedAdsEndpoint : spocs.spocs_endpoint; 1041 let spocsData = []; 1042 let allizomEnabled = spocsEndpoint?.includes("allizom"); 1043 if (spocs.data && spocs.data.newtab_spocs && spocs.data.newtab_spocs.items) { 1044 spocsData = spocs.data.newtab_spocs.items || []; 1045 } 1046 return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", { 1047 colSpan: "2" 1048 }, /*#__PURE__*/external_React_default().createElement("moz-toggle", { 1049 id: "sections-toggle", 1050 disabled: !unifiedAdsSpocsEnabled || null, 1051 pressed: allizomEnabled || null, 1052 onToggle: this.handleAllizomToggle, 1053 label: "Toggle allizom" 1054 }))), /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", { 1055 className: "min" 1056 }, "adsfeed enabled"), /*#__PURE__*/external_React_default().createElement("td", null, adsFeedEnabled ? "true" : "false")), /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", { 1057 className: "min" 1058 }, "spocs endpoint"), /*#__PURE__*/external_React_default().createElement("td", null, spocsEndpoint)), /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", { 1059 className: "min" 1060 }, "Data last fetched"), /*#__PURE__*/external_React_default().createElement("td", null, relativeTime(spocs.lastUpdated))))), /*#__PURE__*/external_React_default().createElement("h4", null, "Spoc data"), /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, spocsData.map(spoc => this.renderStoryData(spoc)))), /*#__PURE__*/external_React_default().createElement("h4", null, "Spoc frequency caps"), /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, spocs.frequency_caps.map(spoc => this.renderStoryData(spoc))))); 1061 } 1062 onStoryToggle(story) { 1063 const { 1064 toggledStories 1065 } = this.state; 1066 this.setState({ 1067 toggledStories: { 1068 ...toggledStories, 1069 [story.id]: !toggledStories[story.id] 1070 } 1071 }); 1072 } 1073 renderStoryData(story) { 1074 let storyData = ""; 1075 if (this.state.toggledStories[story.id]) { 1076 storyData = JSON.stringify(story, null, 2); 1077 } 1078 return /*#__PURE__*/external_React_default().createElement("tr", { 1079 className: "message-item", 1080 key: story.id 1081 }, /*#__PURE__*/external_React_default().createElement("td", { 1082 className: "message-id" 1083 }, /*#__PURE__*/external_React_default().createElement("span", null, story.id, " ", /*#__PURE__*/external_React_default().createElement("br", null)), /*#__PURE__*/external_React_default().createElement(ToggleStoryButton, { 1084 story: story, 1085 onClick: this.onStoryToggle 1086 })), /*#__PURE__*/external_React_default().createElement("td", { 1087 className: "message-summary" 1088 }, /*#__PURE__*/external_React_default().createElement("pre", null, storyData))); 1089 } 1090 renderFeed(feed) { 1091 const { 1092 feeds 1093 } = this.props.state.DiscoveryStream; 1094 if (!feed.url) { 1095 return null; 1096 } 1097 return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", { 1098 className: "min" 1099 }, "Feed url"), /*#__PURE__*/external_React_default().createElement("td", null, feed.url)), /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", { 1100 className: "min" 1101 }, "Data last fetched"), /*#__PURE__*/external_React_default().createElement("td", null, relativeTime(feeds.data[feed.url] ? feeds.data[feed.url].lastUpdated : null) || "(no data)"))); 1102 } 1103 render() { 1104 const prefToggles = "enabled collapsible".split(" "); 1105 const { 1106 config, 1107 layout 1108 } = this.props.state.DiscoveryStream; 1109 const personalized = this.props.otherPrefs["discoverystream.personalization.enabled"]; 1110 const sectionsEnabled = this.props.otherPrefs[PREF_SECTIONS_ENABLED]; 1111 1112 // Prefs for IAB Banners 1113 const mediumRectangleEnabled = this.props.otherPrefs[PREF_AD_SIZE_MEDIUM_RECTANGLE]; 1114 const billboardsEnabled = this.props.otherPrefs[PREF_AD_SIZE_BILLBOARD]; 1115 const leaderboardEnabled = this.props.otherPrefs[PREF_AD_SIZE_LEADERBOARD]; 1116 const spocPlacements = this.props.otherPrefs[PREF_SPOC_PLACEMENTS]; 1117 const mediumRectangleEnabledPressed = mediumRectangleEnabled && spocPlacements.includes("newtab_rectangle"); 1118 const billboardPressed = billboardsEnabled && spocPlacements.includes("newtab_billboard"); 1119 const leaderboardPressed = leaderboardEnabled && spocPlacements.includes("newtab_leaderboard"); 1120 return /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement("button", { 1121 className: "button", 1122 onClick: this.restorePrefDefaults 1123 }, "Restore Pref Defaults"), " ", /*#__PURE__*/external_React_default().createElement("button", { 1124 className: "button", 1125 onClick: this.refreshCache 1126 }, "Refresh Cache"), /*#__PURE__*/external_React_default().createElement("br", null), /*#__PURE__*/external_React_default().createElement("button", { 1127 className: "button", 1128 onClick: this.expireCache 1129 }, "Expire Cache"), " ", /*#__PURE__*/external_React_default().createElement("button", { 1130 className: "button", 1131 onClick: this.systemTick 1132 }, "Trigger System Tick"), " ", /*#__PURE__*/external_React_default().createElement("button", { 1133 className: "button", 1134 onClick: this.idleDaily 1135 }, "Trigger Idle Daily"), /*#__PURE__*/external_React_default().createElement("br", null), /*#__PURE__*/external_React_default().createElement("button", { 1136 className: "button", 1137 onClick: this.refreshInferredPersonalization 1138 }, "Refresh Inferred Personalization"), /*#__PURE__*/external_React_default().createElement("br", null), /*#__PURE__*/external_React_default().createElement("button", { 1139 className: "button", 1140 onClick: this.syncRemoteSettings 1141 }, "Sync Remote Settings"), " ", /*#__PURE__*/external_React_default().createElement("button", { 1142 className: "button", 1143 onClick: this.refreshTopicSelectionCache 1144 }, "Refresh Topic selection count"), /*#__PURE__*/external_React_default().createElement("br", null), /*#__PURE__*/external_React_default().createElement("button", { 1145 className: "button", 1146 onClick: this.showPlaceholder 1147 }, "Show Placeholder Cards"), " ", /*#__PURE__*/external_React_default().createElement("div", { 1148 className: "toggle-wrapper" 1149 }, /*#__PURE__*/external_React_default().createElement("moz-toggle", { 1150 id: "sections-toggle", 1151 pressed: sectionsEnabled || null, 1152 onToggle: this.handleSectionsToggle, 1153 label: "Toggle DS Sections" 1154 })), /*#__PURE__*/external_React_default().createElement("details", { 1155 className: "details-section" 1156 }, /*#__PURE__*/external_React_default().createElement("summary", null, "IAB Banner Ad Sizes"), /*#__PURE__*/external_React_default().createElement("div", { 1157 className: "toggle-wrapper" 1158 }, /*#__PURE__*/external_React_default().createElement("moz-toggle", { 1159 id: "newtab_leaderboard", 1160 pressed: leaderboardPressed || null, 1161 onToggle: this.toggleIABBanners, 1162 label: "Enable IAB Leaderboard" 1163 })), /*#__PURE__*/external_React_default().createElement("div", { 1164 className: "toggle-wrapper" 1165 }, /*#__PURE__*/external_React_default().createElement("moz-toggle", { 1166 id: "newtab_billboard", 1167 pressed: billboardPressed || null, 1168 onToggle: this.toggleIABBanners, 1169 label: "Enable IAB Billboard" 1170 })), /*#__PURE__*/external_React_default().createElement("div", { 1171 className: "toggle-wrapper" 1172 }, /*#__PURE__*/external_React_default().createElement("moz-toggle", { 1173 id: "newtab_rectangle", 1174 pressed: mediumRectangleEnabledPressed || null, 1175 onToggle: this.toggleIABBanners, 1176 label: "Enable IAB Medium Rectangle (MREC)" 1177 }))), /*#__PURE__*/external_React_default().createElement("button", { 1178 className: "button", 1179 onClick: this.sendConversionEvent 1180 }, "Send conversion event"), /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, prefToggles.map(pref => /*#__PURE__*/external_React_default().createElement(Row, { 1181 key: pref 1182 }, /*#__PURE__*/external_React_default().createElement("td", null, /*#__PURE__*/external_React_default().createElement(TogglePrefCheckbox, { 1183 checked: config[pref], 1184 pref: pref, 1185 onChange: this.setConfigValue 1186 })))))), /*#__PURE__*/external_React_default().createElement("h3", null, "Layout"), layout.map((row, rowIndex) => /*#__PURE__*/external_React_default().createElement("div", { 1187 key: `row-${rowIndex}` 1188 }, row.components.map((component, componentIndex) => /*#__PURE__*/external_React_default().createElement("div", { 1189 key: `component-${componentIndex}`, 1190 className: "ds-component" 1191 }, this.renderComponent(row.width, component))))), /*#__PURE__*/external_React_default().createElement("h3", null, "Personalization"), /*#__PURE__*/external_React_default().createElement(Personalization, { 1192 personalized: personalized, 1193 dispatch: this.props.dispatch, 1194 state: { 1195 Personalization: this.props.state.Personalization 1196 } 1197 }), /*#__PURE__*/external_React_default().createElement("h3", null, "Spocs"), this.renderSpocs(), /*#__PURE__*/external_React_default().createElement("h3", null, "Feeds Data"), /*#__PURE__*/external_React_default().createElement("div", { 1198 className: "large-data-container" 1199 }, this.renderFeedsData()), /*#__PURE__*/external_React_default().createElement("h3", null, "Impressions Data"), /*#__PURE__*/external_React_default().createElement("div", { 1200 className: "large-data-container" 1201 }, this.renderImpressionsData()), /*#__PURE__*/external_React_default().createElement("h3", null, "Blocked Data"), /*#__PURE__*/external_React_default().createElement("div", { 1202 className: "large-data-container" 1203 }, this.renderBlocksData()), /*#__PURE__*/external_React_default().createElement("h3", null, "Weather Data"), this.renderWeatherData(), /*#__PURE__*/external_React_default().createElement("h3", null, "Personalization Data"), this.renderPersonalizationData()); 1204 } 1205 } 1206 class DiscoveryStreamAdminInner extends (external_React_default()).PureComponent { 1207 constructor(props) { 1208 super(props); 1209 this.setState = this.setState.bind(this); 1210 } 1211 render() { 1212 return /*#__PURE__*/external_React_default().createElement("div", { 1213 className: `discoverystream-admin ${this.props.collapsed ? "collapsed" : "expanded"}` 1214 }, /*#__PURE__*/external_React_default().createElement("main", { 1215 className: "main-panel" 1216 }, /*#__PURE__*/external_React_default().createElement("h1", null, "Discovery Stream Admin"), /*#__PURE__*/external_React_default().createElement("p", { 1217 className: "helpLink" 1218 }, /*#__PURE__*/external_React_default().createElement("span", { 1219 className: "icon icon-small-spacer icon-info" 1220 }), " ", /*#__PURE__*/external_React_default().createElement("span", null, "Need to access the ASRouter Admin dev tools?", " ", /*#__PURE__*/external_React_default().createElement("a", { 1221 target: "blank", 1222 href: "about:asrouter" 1223 }, "Click here"))), /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement(DiscoveryStreamAdminUI, { 1224 state: { 1225 DiscoveryStream: this.props.DiscoveryStream, 1226 Personalization: this.props.Personalization, 1227 Weather: this.props.Weather, 1228 InferredPersonalization: this.props.InferredPersonalization 1229 }, 1230 otherPrefs: this.props.Prefs.values, 1231 dispatch: this.props.dispatch 1232 })))); 1233 } 1234 } 1235 function CollapseToggle(props) { 1236 const { 1237 devtoolsCollapsed 1238 } = props; 1239 const label = `${devtoolsCollapsed ? "Expand" : "Collapse"} devtools`; 1240 (0,external_React_namespaceObject.useEffect)(() => { 1241 // Set or remove body class depending on devtoolsCollapsed state 1242 if (devtoolsCollapsed) { 1243 globalThis.document.body.classList.remove("no-scroll"); 1244 } else { 1245 globalThis.document.body.classList.add("no-scroll"); 1246 } 1247 1248 // Cleanup on unmount 1249 return () => { 1250 globalThis.document.body.classList.remove("no-scroll"); 1251 }; 1252 }, [devtoolsCollapsed]); 1253 return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("a", { 1254 href: devtoolsCollapsed ? "#devtools" : "#", 1255 title: label, 1256 "aria-label": label, 1257 className: `discoverystream-admin-toggle ${devtoolsCollapsed ? "expanded" : "collapsed"}` 1258 }, /*#__PURE__*/external_React_default().createElement("span", { 1259 className: "icon icon-devtools" 1260 })), !devtoolsCollapsed ? /*#__PURE__*/external_React_default().createElement(DiscoveryStreamAdminInner, _extends({}, props, { 1261 collapsed: devtoolsCollapsed 1262 })) : null); 1263 } 1264 const _DiscoveryStreamAdmin = props => /*#__PURE__*/external_React_default().createElement(CollapseToggle, props); 1265 const DiscoveryStreamAdmin = (0,external_ReactRedux_namespaceObject.connect)(state => ({ 1266 Sections: state.Sections, 1267 DiscoveryStream: state.DiscoveryStream, 1268 Personalization: state.Personalization, 1269 InferredPersonalization: state.InferredPersonalization, 1270 Prefs: state.Prefs, 1271 Weather: state.Weather 1272 }))(_DiscoveryStreamAdmin); 1273 ;// CONCATENATED MODULE: ./content-src/components/ConfirmDialog/ConfirmDialog.jsx 1274 /* This Source Code Form is subject to the terms of the Mozilla Public 1275 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1276 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1277 1278 1279 1280 1281 1282 /** 1283 * ConfirmDialog component. 1284 * One primary action button, one cancel button. 1285 * 1286 * Content displayed is controlled by `data` prop the component receives. 1287 * Example: 1288 * data: { 1289 * // Any sort of data needed to be passed around by actions. 1290 * payload: site.url, 1291 * // Primary button AlsoToMain action. 1292 * action: "DELETE_HISTORY_URL", 1293 * // Primary button USerEvent action. 1294 * userEvent: "DELETE", 1295 * // Array of locale ids to display. 1296 * message_body: ["confirm_history_delete_p1", "confirm_history_delete_notice_p2"], 1297 * // Text for primary button. 1298 * confirm_button_string_id: "menu_action_delete" 1299 * }, 1300 */ 1301 class _ConfirmDialog extends (external_React_default()).PureComponent { 1302 constructor(props) { 1303 super(props); 1304 this._handleCancelBtn = this._handleCancelBtn.bind(this); 1305 this._handleConfirmBtn = this._handleConfirmBtn.bind(this); 1306 this.dialogRef = /*#__PURE__*/external_React_default().createRef(); 1307 } 1308 componentDidUpdate() { 1309 const dialogElement = this.dialogRef.current; 1310 if (!dialogElement) { 1311 return; 1312 } 1313 1314 // Open dialog when visible becomes true 1315 if (this.props.visible && !dialogElement.open) { 1316 dialogElement.showModal(); 1317 } 1318 // Close dialog when visible becomes false 1319 else if (!this.props.visible && dialogElement.open) { 1320 dialogElement.close(); 1321 } 1322 } 1323 _handleCancelBtn() { 1324 this.props.dispatch({ 1325 type: actionTypes.DIALOG_CANCEL 1326 }); 1327 this.props.dispatch(actionCreators.UserEvent({ 1328 event: actionTypes.DIALOG_CANCEL, 1329 source: this.props.data.eventSource 1330 })); 1331 } 1332 _handleConfirmBtn() { 1333 this.props.data.onConfirm.forEach(this.props.dispatch); 1334 } 1335 _renderModalMessage() { 1336 const message_body = this.props.data.body_string_id; 1337 if (!message_body) { 1338 return null; 1339 } 1340 return /*#__PURE__*/external_React_default().createElement("span", null, message_body.map(msg => /*#__PURE__*/external_React_default().createElement("p", { 1341 key: msg, 1342 "data-l10n-id": msg 1343 }))); 1344 } 1345 render() { 1346 return /*#__PURE__*/external_React_default().createElement("dialog", { 1347 ref: this.dialogRef, 1348 className: "confirmation-dialog", 1349 onClick: e => { 1350 // Close modal when clicking on the backdrop pseudo element (the background of the modal) 1351 if (e.target === this.dialogRef.current) { 1352 this._handleCancelBtn(); 1353 } 1354 } 1355 }, /*#__PURE__*/external_React_default().createElement("div", { 1356 className: "modal" 1357 }, /*#__PURE__*/external_React_default().createElement("section", { 1358 className: "modal-message" 1359 }, this.props.data.icon && /*#__PURE__*/external_React_default().createElement("span", { 1360 className: `icon icon-spacer icon-${this.props.data.icon}` 1361 }), this._renderModalMessage()), /*#__PURE__*/external_React_default().createElement("section", { 1362 className: "button-group" 1363 }, /*#__PURE__*/external_React_default().createElement("moz-button-group", null, /*#__PURE__*/external_React_default().createElement("moz-button", { 1364 onClick: this._handleCancelBtn, 1365 "data-l10n-id": this.props.data.cancel_button_string_id 1366 }), /*#__PURE__*/external_React_default().createElement("moz-button", { 1367 type: "primary", 1368 onClick: this._handleConfirmBtn, 1369 "data-l10n-id": this.props.data.confirm_button_string_id, 1370 "data-l10n-args": JSON.stringify(this.props.data.confirm_button_string_args) 1371 }))))); 1372 } 1373 } 1374 const ConfirmDialog = (0,external_ReactRedux_namespaceObject.connect)(state => state.Dialog)(_ConfirmDialog); 1375 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx 1376 /* This Source Code Form is subject to the terms of the Mozilla Public 1377 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1378 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1379 1380 1381 const PLACEHOLDER_IMAGE_DATA_ARRAY = [{ 1382 rotation: "0deg", 1383 offsetx: "20px", 1384 offsety: "8px", 1385 scale: "45%" 1386 }, { 1387 rotation: "54deg", 1388 offsetx: "-26px", 1389 offsety: "62px", 1390 scale: "55%" 1391 }, { 1392 rotation: "-30deg", 1393 offsetx: "78px", 1394 offsety: "30px", 1395 scale: "68%" 1396 }, { 1397 rotation: "-22deg", 1398 offsetx: "0", 1399 offsety: "92px", 1400 scale: "60%" 1401 }, { 1402 rotation: "-65deg", 1403 offsetx: "66px", 1404 offsety: "28px", 1405 scale: "60%" 1406 }, { 1407 rotation: "22deg", 1408 offsetx: "-35px", 1409 offsety: "62px", 1410 scale: "52%" 1411 }, { 1412 rotation: "-25deg", 1413 offsetx: "86px", 1414 offsety: "-15px", 1415 scale: "68%" 1416 }]; 1417 const PLACEHOLDER_IMAGE_COLORS_ARRAY = "#0090ED #FF4F5F #2AC3A2 #FF7139 #A172FF #FFA437 #FF2A8A".split(" "); 1418 function generateIndex({ 1419 keyCode, 1420 max 1421 }) { 1422 if (!keyCode) { 1423 // Just grab a random index if we cannot generate an index from a key. 1424 return Math.floor(Math.random() * max); 1425 } 1426 const hashStr = str => { 1427 let hash = 0; 1428 for (let i = 0; i < str.length; i++) { 1429 let charCode = str.charCodeAt(i); 1430 hash += charCode; 1431 } 1432 return hash; 1433 }; 1434 const hash = hashStr(keyCode); 1435 return hash % max; 1436 } 1437 function PlaceholderImage({ 1438 urlKey, 1439 titleKey 1440 }) { 1441 const dataIndex = generateIndex({ 1442 keyCode: urlKey, 1443 max: PLACEHOLDER_IMAGE_DATA_ARRAY.length 1444 }); 1445 const colorIndex = generateIndex({ 1446 keyCode: titleKey, 1447 max: PLACEHOLDER_IMAGE_COLORS_ARRAY.length 1448 }); 1449 const { 1450 rotation, 1451 offsetx, 1452 offsety, 1453 scale 1454 } = PLACEHOLDER_IMAGE_DATA_ARRAY[dataIndex]; 1455 const color = PLACEHOLDER_IMAGE_COLORS_ARRAY[colorIndex]; 1456 const style = { 1457 "--placeholderBackgroundColor": color, 1458 "--placeholderBackgroundRotation": rotation, 1459 "--placeholderBackgroundOffsetx": offsetx, 1460 "--placeholderBackgroundOffsety": offsety, 1461 "--placeholderBackgroundScale": scale 1462 }; 1463 return /*#__PURE__*/external_React_default().createElement("div", { 1464 style: style, 1465 className: "placeholder-image" 1466 }); 1467 } 1468 class DSImage extends (external_React_default()).PureComponent { 1469 constructor(props) { 1470 super(props); 1471 this.onOptimizedImageError = this.onOptimizedImageError.bind(this); 1472 this.onNonOptimizedImageError = this.onNonOptimizedImageError.bind(this); 1473 this.onLoad = this.onLoad.bind(this); 1474 this.state = { 1475 isLoaded: false, 1476 optimizedImageFailed: false, 1477 useTransition: false 1478 }; 1479 } 1480 onIdleCallback() { 1481 if (!this.state.isLoaded) { 1482 this.setState({ 1483 useTransition: true 1484 }); 1485 } 1486 } 1487 1488 // Wraps the image url with the Pocket proxy to both resize and crop the image. 1489 reformatImageURL(url, width, height) { 1490 const smart = this.props.smartCrop ? "smart/" : ""; 1491 // Change the image URL to request a size tailored for the parent container width 1492 // Also: force JPEG, quality 60, no upscaling, no EXIF data 1493 // Uses Thumbor: https://thumbor.readthedocs.io/en/latest/usage.html 1494 const formattedUrl = `https://img-getpocket.cdn.mozilla.net/${width}x${height}/${smart}filters:format(jpeg):quality(60):no_upscale():strip_exif()/${encodeURIComponent(url)}`; 1495 return this.secureImageURL(formattedUrl); 1496 } 1497 1498 // Wraps the image URL with the moz-cached-ohttp:// protocol. 1499 // This enables Firefox to load resources over Oblivious HTTP (OHTTP), 1500 // providing privacy-preserving resource loading. 1501 // Applied only when inferred personalization is enabled. 1502 // See: https://firefox-source-docs.mozilla.org/browser/components/mozcachedohttp/docs/index.html 1503 secureImageURL(url) { 1504 if (!this.props.secureImage) { 1505 return url; 1506 } 1507 return `moz-cached-ohttp://newtab-image/?url=${encodeURIComponent(url)}`; 1508 } 1509 componentDidMount() { 1510 this.idleCallbackId = this.props.windowObj.requestIdleCallback(this.onIdleCallback.bind(this)); 1511 } 1512 componentWillUnmount() { 1513 if (this.idleCallbackId) { 1514 this.props.windowObj.cancelIdleCallback(this.idleCallbackId); 1515 } 1516 } 1517 render() { 1518 let classNames = `ds-image 1519 ${this.props.extraClassNames ? ` ${this.props.extraClassNames}` : ``} 1520 ${this.state && this.state.useTransition ? ` use-transition` : ``} 1521 ${this.state && this.state.isLoaded ? ` loaded` : ``} 1522 `; 1523 let img; 1524 if (this.state) { 1525 if (this.props.optimize && this.props.rawSource && !this.state.optimizedImageFailed) { 1526 const baseSource = this.props.rawSource; 1527 1528 // We don't care about securing this.props.source, as this exclusivly 1529 // comes from an older service that is not personalized. 1530 // This can also return a non secure url if this functionality is not enabled. 1531 const securedSource = this.secureImageURL(baseSource); 1532 let sizeRules = []; 1533 let srcSetRules = []; 1534 for (let rule of this.props.sizes) { 1535 let { 1536 mediaMatcher, 1537 width, 1538 height 1539 } = rule; 1540 let sizeRule = `${mediaMatcher} ${width}px`; 1541 sizeRules.push(sizeRule); 1542 let srcSetRule = `${this.reformatImageURL(baseSource, width, height)} ${width}w`; 1543 let srcSetRule2x = `${this.reformatImageURL(baseSource, width * 2, height * 2)} ${width * 2}w`; 1544 srcSetRules.push(srcSetRule); 1545 srcSetRules.push(srcSetRule2x); 1546 } 1547 if (this.props.sizes.length) { 1548 // We have to supply a fallback in the very unlikely event that none of 1549 // the media queries match. The smallest dimension was chosen arbitrarily. 1550 sizeRules.push(`${this.props.sizes[this.props.sizes.length - 1].width}px`); 1551 } 1552 img = /*#__PURE__*/external_React_default().createElement("img", { 1553 loading: "lazy", 1554 alt: this.props.alt_text, 1555 crossOrigin: "anonymous", 1556 onLoad: this.onLoad, 1557 onError: this.onOptimizedImageError, 1558 sizes: sizeRules.join(","), 1559 src: securedSource, 1560 srcSet: srcSetRules.join(",") 1561 }); 1562 } else if (this.props.source && !this.state.nonOptimizedImageFailed) { 1563 img = /*#__PURE__*/external_React_default().createElement("img", { 1564 loading: "lazy", 1565 alt: this.props.alt_text, 1566 crossOrigin: "anonymous", 1567 onLoad: this.onLoad, 1568 onError: this.onNonOptimizedImageError, 1569 src: this.props.source 1570 }); 1571 } else { 1572 // We consider a failed to load img or source without an image as loaded. 1573 classNames = `${classNames} loaded`; 1574 // Remove the img element if we have no source. Render a placeholder instead. 1575 // This only happens for recent saves without a source. 1576 if (this.props.isRecentSave && !this.props.rawSource && !this.props.source) { 1577 img = /*#__PURE__*/external_React_default().createElement(PlaceholderImage, { 1578 urlKey: this.props.url, 1579 titleKey: this.props.title 1580 }); 1581 } else { 1582 img = /*#__PURE__*/external_React_default().createElement("div", { 1583 className: "broken-image" 1584 }); 1585 } 1586 } 1587 } 1588 return /*#__PURE__*/external_React_default().createElement("picture", { 1589 className: classNames 1590 }, img); 1591 } 1592 onOptimizedImageError() { 1593 // This will trigger a re-render and the unoptimized 450px image will be used as a fallback 1594 this.setState({ 1595 optimizedImageFailed: true 1596 }); 1597 } 1598 onNonOptimizedImageError() { 1599 this.setState({ 1600 nonOptimizedImageFailed: true 1601 }); 1602 } 1603 onLoad() { 1604 this.setState({ 1605 isLoaded: true 1606 }); 1607 } 1608 } 1609 DSImage.defaultProps = { 1610 source: null, 1611 // The current source style from Pocket API (always 450px) 1612 rawSource: null, 1613 // Unadulterated image URL to filter through Thumbor 1614 extraClassNames: null, 1615 // Additional classnames to append to component 1616 optimize: true, 1617 // Measure parent container to request exact sizes 1618 alt_text: null, 1619 windowObj: window, 1620 // Added to support unit tests 1621 sizes: [] 1622 }; 1623 ;// CONCATENATED MODULE: ./content-src/components/ContextMenu/ContextMenu.jsx 1624 /* This Source Code Form is subject to the terms of the Mozilla Public 1625 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1626 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1627 1628 1629 1630 class ContextMenu extends (external_React_default()).PureComponent { 1631 constructor(props) { 1632 super(props); 1633 this.hideContext = this.hideContext.bind(this); 1634 this.onShow = this.onShow.bind(this); 1635 this.onClick = this.onClick.bind(this); 1636 } 1637 hideContext() { 1638 this.props.onUpdate(false); 1639 } 1640 onShow() { 1641 if (this.props.onShow) { 1642 this.props.onShow(); 1643 } 1644 } 1645 componentDidMount() { 1646 this.onShow(); 1647 setTimeout(() => { 1648 globalThis.addEventListener("click", this.hideContext); 1649 }, 0); 1650 } 1651 componentWillUnmount() { 1652 globalThis.removeEventListener("click", this.hideContext); 1653 } 1654 onClick(event) { 1655 // Eat all clicks on the context menu so they don't bubble up to window. 1656 // This prevents the context menu from closing when clicking disabled items 1657 // or the separators. 1658 event.stopPropagation(); 1659 } 1660 render() { 1661 // Disabling focus on the menu span allows the first tab to focus on the first menu item instead of the wrapper. 1662 return ( 1663 /*#__PURE__*/ 1664 // eslint-disable-next-line jsx-a11y/interactive-supports-focus 1665 external_React_default().createElement("span", { 1666 className: "context-menu" 1667 }, /*#__PURE__*/external_React_default().createElement("ul", { 1668 role: "menu", 1669 onClick: this.onClick, 1670 onKeyDown: this.onClick, 1671 className: "context-menu-list" 1672 }, this.props.options.map((option, i) => option.type === "separator" ? /*#__PURE__*/external_React_default().createElement("li", { 1673 key: i, 1674 className: "separator", 1675 role: "separator" 1676 }) : option.type !== "empty" && /*#__PURE__*/external_React_default().createElement(ContextMenuItem, { 1677 key: i, 1678 option: option, 1679 hideContext: this.hideContext, 1680 keyboardAccess: this.props.keyboardAccess 1681 })))) 1682 ); 1683 } 1684 } 1685 class _ContextMenuItem extends (external_React_default()).PureComponent { 1686 constructor(props) { 1687 super(props); 1688 this.onClick = this.onClick.bind(this); 1689 this.onKeyDown = this.onKeyDown.bind(this); 1690 this.onKeyUp = this.onKeyUp.bind(this); 1691 this.focusFirst = this.focusFirst.bind(this); 1692 } 1693 onClick(event) { 1694 this.props.hideContext(); 1695 this.props.option.onClick(event); 1696 } 1697 1698 // Focus the first menu item if the menu was accessed via the keyboard. 1699 focusFirst(button) { 1700 if (this.props.keyboardAccess && button) { 1701 button.focus(); 1702 } 1703 } 1704 1705 // This selects the correct node based on the key pressed 1706 focusSibling(target, key) { 1707 const { 1708 parentNode 1709 } = target; 1710 const closestSiblingSelector = key === "ArrowUp" ? "previousSibling" : "nextSibling"; 1711 if (!parentNode[closestSiblingSelector]) { 1712 return; 1713 } 1714 if (parentNode[closestSiblingSelector].firstElementChild) { 1715 parentNode[closestSiblingSelector].firstElementChild.focus(); 1716 } else { 1717 parentNode[closestSiblingSelector][closestSiblingSelector].firstElementChild.focus(); 1718 } 1719 } 1720 onKeyDown(event) { 1721 const { 1722 option 1723 } = this.props; 1724 switch (event.key) { 1725 case "Tab": 1726 // tab goes down in context menu, shift + tab goes up in context menu 1727 // if we're on the last item, one more tab will close the context menu 1728 // similarly, if we're on the first item, one more shift + tab will close it 1729 if (event.shiftKey && option.first || !event.shiftKey && option.last) { 1730 this.props.hideContext(); 1731 } 1732 break; 1733 case "ArrowUp": 1734 case "ArrowDown": 1735 event.preventDefault(); 1736 this.focusSibling(event.target, event.key); 1737 break; 1738 case "Enter": 1739 case " ": 1740 event.preventDefault(); 1741 this.props.hideContext(); 1742 option.onClick(); 1743 break; 1744 case "Escape": 1745 this.props.hideContext(); 1746 break; 1747 } 1748 } 1749 1750 // Prevents the default behavior of spacebar 1751 // scrolling the page & auto-triggering buttons. 1752 onKeyUp(event) { 1753 if (event.key === " ") { 1754 event.preventDefault(); 1755 } 1756 } 1757 render() { 1758 const { 1759 option 1760 } = this.props; 1761 const className = [option.disabled ? "disabled" : ""].join(" "); 1762 return /*#__PURE__*/external_React_default().createElement("li", { 1763 role: "presentation", 1764 className: "context-menu-item" 1765 }, /*#__PURE__*/external_React_default().createElement("button", { 1766 role: "menuitem", 1767 className: className, 1768 onClick: this.onClick, 1769 onKeyDown: this.onKeyDown, 1770 onKeyUp: this.onKeyUp, 1771 ref: option.first ? this.focusFirst : null, 1772 "aria-haspopup": option.id === "newtab-menu-edit-topsites" ? "dialog" : null 1773 }, /*#__PURE__*/external_React_default().createElement("span", { 1774 "data-l10n-id": option.string_id || option.id 1775 }))); 1776 } 1777 } 1778 const ContextMenuItem = (0,external_ReactRedux_namespaceObject.connect)(state => ({ 1779 Prefs: state.Prefs 1780 }))(_ContextMenuItem); 1781 ;// CONCATENATED MODULE: ./content-src/lib/link-menu-options.mjs 1782 /* This Source Code Form is subject to the terms of the Mozilla Public 1783 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1784 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1785 1786 1787 1788 const _OpenInPrivateWindow = site => ({ 1789 id: "newtab-menu-open-new-private-window", 1790 icon: "new-window-private", 1791 action: actionCreators.OnlyToMain({ 1792 type: actionTypes.OPEN_PRIVATE_WINDOW, 1793 data: { 1794 url: site.url, 1795 referrer: site.referrer, 1796 event_source: "CONTEXT_MENU", 1797 }, 1798 }), 1799 userEvent: "OPEN_PRIVATE_WINDOW", 1800 }); 1801 1802 /** 1803 * List of functions that return items that can be included as menu options in a 1804 * LinkMenu. All functions take the site as the first parameter, and optionally 1805 * the index of the site. 1806 */ 1807 const LinkMenuOptions = { 1808 Separator: () => ({ type: "separator" }), 1809 EmptyItem: () => ({ type: "empty" }), 1810 ShowPrivacyInfo: () => ({ 1811 id: "newtab-menu-show-privacy-info", 1812 icon: "info", 1813 action: { 1814 type: actionTypes.SHOW_PRIVACY_INFO, 1815 }, 1816 userEvent: "SHOW_PRIVACY_INFO", 1817 }), 1818 AboutSponsored: site => ({ 1819 id: "newtab-menu-show-privacy-info", 1820 icon: "info", 1821 action: actionCreators.AlsoToMain({ 1822 type: actionTypes.ABOUT_SPONSORED_TOP_SITES, 1823 data: { 1824 advertiser_name: (site.label || site.hostname).toLocaleLowerCase(), 1825 position: site.sponsored_position, 1826 tile_id: site.sponsored_tile_id, 1827 block_key: site.block_key, 1828 }, 1829 }), 1830 userEvent: "TOPSITE_SPONSOR_INFO", 1831 }), 1832 RemoveBookmark: site => ({ 1833 id: "newtab-menu-remove-bookmark", 1834 icon: "bookmark-added", 1835 action: actionCreators.AlsoToMain({ 1836 type: actionTypes.DELETE_BOOKMARK_BY_ID, 1837 data: site.bookmarkGuid, 1838 }), 1839 userEvent: "BOOKMARK_DELETE", 1840 }), 1841 AddBookmark: site => ({ 1842 id: "newtab-menu-bookmark", 1843 icon: "bookmark-hollow", 1844 action: actionCreators.AlsoToMain({ 1845 type: actionTypes.BOOKMARK_URL, 1846 data: { url: site.url, title: site.title, type: site.type }, 1847 }), 1848 userEvent: "BOOKMARK_ADD", 1849 }), 1850 OpenInNewWindow: site => ({ 1851 id: "newtab-menu-open-new-window", 1852 icon: "new-window", 1853 action: actionCreators.AlsoToMain({ 1854 type: actionTypes.OPEN_NEW_WINDOW, 1855 data: { 1856 card_type: site.card_type, 1857 referrer: site.referrer, 1858 typedBonus: site.typedBonus, 1859 url: site.url, 1860 is_sponsored: !!site.sponsored_tile_id, 1861 event_source: "CONTEXT_MENU", 1862 topic: site.topic, 1863 firstVisibleTimestamp: site.firstVisibleTimestamp, 1864 tile_id: site.tile_id, 1865 recommendation_id: site.recommendation_id, 1866 scheduled_corpus_item_id: site.scheduled_corpus_item_id, 1867 corpus_item_id: site.corpus_item_id, 1868 received_rank: site.received_rank, 1869 recommended_at: site.recommended_at, 1870 format: site.format, 1871 ...(site.flight_id ? { flight_id: site.flight_id } : {}), 1872 is_pocket_card: site.type === "CardGrid", 1873 ...(site.section 1874 ? { 1875 section: site.section, 1876 section_position: site.section_position, 1877 is_section_followed: site.is_section_followed, 1878 } 1879 : {}), 1880 }, 1881 }), 1882 userEvent: "OPEN_NEW_WINDOW", 1883 }), 1884 1885 // This blocks the url for regular stories, 1886 // but also sends a message to DiscoveryStream with flight_id. 1887 // If DiscoveryStream sees this message for a flight_id 1888 // it also blocks it on the flight_id. 1889 BlockUrl: (site, index, eventSource) => { 1890 return LinkMenuOptions.BlockUrls([site], index, eventSource); 1891 }, 1892 // Same as BlockUrl, except can work on an array of sites. 1893 BlockUrls: (tiles, pos, eventSource) => ({ 1894 id: "newtab-menu-dismiss", 1895 icon: "dismiss", 1896 action: actionCreators.AlsoToMain({ 1897 type: actionTypes.BLOCK_URL, 1898 source: eventSource, 1899 data: tiles.map(site => ({ 1900 url: site.original_url || site.open_url || site.url, 1901 // pocket_id is only for pocket stories being in highlights, and then dismissed. 1902 pocket_id: site.pocket_id, 1903 tile_id: site.tile_id, 1904 ...(site.block_key ? { block_key: site.block_key } : {}), 1905 recommendation_id: site.recommendation_id, 1906 scheduled_corpus_item_id: site.scheduled_corpus_item_id, 1907 corpus_item_id: site.corpus_item_id, 1908 received_rank: site.received_rank, 1909 recommended_at: site.recommended_at, 1910 // used by PlacesFeed and TopSitesFeed for sponsored top sites blocking. 1911 isSponsoredTopSite: site.sponsored_position, 1912 type: site.type, 1913 card_type: site.card_type, 1914 ...(site.shim && site.shim.delete ? { shim: site.shim.delete } : {}), 1915 ...(site.flight_id ? { flight_id: site.flight_id } : {}), 1916 // If not sponsored, hostname could be anything (Cat3 Data!). 1917 // So only put in advertiser_name for sponsored topsites. 1918 ...(site.sponsored_position 1919 ? { 1920 advertiser_name: ( 1921 site.label || site.hostname 1922 )?.toLocaleLowerCase(), 1923 } 1924 : {}), 1925 position: pos, 1926 ...(site.sponsored_tile_id ? { tile_id: site.sponsored_tile_id } : {}), 1927 is_pocket_card: site.type === "CardGrid", 1928 ...(site.format ? { format: site.format } : {}), 1929 ...(site.section 1930 ? { 1931 section: site.section, 1932 section_position: site.section_position, 1933 is_section_followed: site.is_section_followed, 1934 } 1935 : {}), 1936 })), 1937 }), 1938 impression: actionCreators.ImpressionStats({ 1939 source: eventSource, 1940 block: 0, 1941 tiles: tiles.map((site, index) => ({ 1942 id: site.guid, 1943 pos: pos + index, 1944 ...(site.shim && site.shim.delete ? { shim: site.shim.delete } : {}), 1945 })), 1946 }), 1947 userEvent: "BLOCK", 1948 }), 1949 1950 // This is the "Dismiss" action for leaderboard/billboard ads. 1951 BlockAdUrl: (site, pos, eventSource) => ({ 1952 id: "newtab-menu-dismiss", 1953 icon: "dismiss", 1954 action: actionCreators.AlsoToMain({ 1955 type: actionTypes.BLOCK_URL, 1956 data: [site], 1957 }), 1958 impression: actionCreators.ImpressionStats({ 1959 source: eventSource, 1960 block: 0, 1961 tiles: [ 1962 { 1963 id: site.guid, 1964 pos, 1965 ...(site.shim && site.shim.save ? { shim: site.shim.save } : {}), 1966 }, 1967 ], 1968 }), 1969 userEvent: "BLOCK", 1970 }), 1971 1972 // This is an option for web extentions which will result in remove items from 1973 // memory and notify the web extenion, rather than using the built-in block list. 1974 WebExtDismiss: (site, index, eventSource) => ({ 1975 id: "menu_action_webext_dismiss", 1976 string_id: "newtab-menu-dismiss", 1977 icon: "dismiss", 1978 action: actionCreators.WebExtEvent(actionTypes.WEBEXT_DISMISS, { 1979 source: eventSource, 1980 url: site.url, 1981 action_position: index, 1982 }), 1983 }), 1984 DeleteUrl: (site, index, eventSource, isEnabled, siteInfo) => ({ 1985 id: "newtab-menu-delete-history", 1986 icon: "delete", 1987 action: { 1988 type: actionTypes.DIALOG_OPEN, 1989 data: { 1990 onConfirm: [ 1991 actionCreators.AlsoToMain({ 1992 type: actionTypes.DELETE_HISTORY_URL, 1993 data: { 1994 url: site.url, 1995 pocket_id: site.pocket_id, 1996 forceBlock: site.bookmarkGuid, 1997 }, 1998 }), 1999 actionCreators.UserEvent( 2000 Object.assign( 2001 { event: "DELETE", source: eventSource, action_position: index }, 2002 siteInfo 2003 ) 2004 ), 2005 // Also broadcast that this url has been deleted so that 2006 // the confirmation dialog knows it needs to disappear now. 2007 actionCreators.AlsoToMain({ 2008 type: actionTypes.DIALOG_CLOSE, 2009 }), 2010 ], 2011 eventSource, 2012 body_string_id: [ 2013 "newtab-confirm-delete-history-p1", 2014 "newtab-confirm-delete-history-p2", 2015 ], 2016 confirm_button_string_id: "newtab-topsites-delete-history-button", 2017 cancel_button_string_id: "newtab-topsites-cancel-button", 2018 icon: "modal-delete", 2019 }, 2020 }, 2021 userEvent: "DIALOG_OPEN", 2022 }), 2023 ShowFile: site => ({ 2024 id: "newtab-menu-show-file", 2025 icon: "search", 2026 action: actionCreators.OnlyToMain({ 2027 type: actionTypes.SHOW_DOWNLOAD_FILE, 2028 data: { url: site.url }, 2029 }), 2030 }), 2031 OpenFile: site => ({ 2032 id: "newtab-menu-open-file", 2033 icon: "open-file", 2034 action: actionCreators.OnlyToMain({ 2035 type: actionTypes.OPEN_DOWNLOAD_FILE, 2036 data: { url: site.url }, 2037 }), 2038 }), 2039 CopyDownloadLink: site => ({ 2040 id: "newtab-menu-copy-download-link", 2041 icon: "copy", 2042 action: actionCreators.OnlyToMain({ 2043 type: actionTypes.COPY_DOWNLOAD_LINK, 2044 data: { url: site.url }, 2045 }), 2046 }), 2047 GoToDownloadPage: site => ({ 2048 id: "newtab-menu-go-to-download-page", 2049 icon: "download", 2050 action: actionCreators.OnlyToMain({ 2051 type: actionTypes.OPEN_LINK, 2052 data: { url: site.referrer }, 2053 }), 2054 disabled: !site.referrer, 2055 }), 2056 RemoveDownload: site => ({ 2057 id: "newtab-menu-remove-download", 2058 icon: "delete", 2059 action: actionCreators.OnlyToMain({ 2060 type: actionTypes.REMOVE_DOWNLOAD_FILE, 2061 data: { url: site.url }, 2062 }), 2063 }), 2064 PinTopSite: (site, index) => ({ 2065 id: "newtab-menu-pin", 2066 icon: "pin", 2067 action: actionCreators.AlsoToMain({ 2068 type: actionTypes.TOP_SITES_PIN, 2069 data: { 2070 site, 2071 index, 2072 }, 2073 }), 2074 userEvent: "PIN", 2075 }), 2076 UnpinTopSite: site => ({ 2077 id: "newtab-menu-unpin", 2078 icon: "unpin", 2079 action: actionCreators.AlsoToMain({ 2080 type: actionTypes.TOP_SITES_UNPIN, 2081 data: { site: { url: site.url } }, 2082 }), 2083 userEvent: "UNPIN", 2084 }), 2085 EditTopSite: (site, index) => ({ 2086 id: "newtab-menu-edit-topsites", 2087 icon: "edit", 2088 action: { 2089 type: actionTypes.TOP_SITES_EDIT, 2090 data: { index }, 2091 }, 2092 }), 2093 CheckBookmark: site => 2094 site.bookmarkGuid 2095 ? LinkMenuOptions.RemoveBookmark(site) 2096 : LinkMenuOptions.AddBookmark(site), 2097 CheckPinTopSite: (site, index) => 2098 site.isPinned 2099 ? LinkMenuOptions.UnpinTopSite(site) 2100 : LinkMenuOptions.PinTopSite(site, index), 2101 OpenInPrivateWindow: (site, index, eventSource, isEnabled) => 2102 isEnabled ? _OpenInPrivateWindow(site) : LinkMenuOptions.EmptyItem(), 2103 ChangeWeatherLocation: () => ({ 2104 id: "newtab-weather-menu-change-location", 2105 action: actionCreators.BroadcastToContent({ 2106 type: actionTypes.WEATHER_SEARCH_ACTIVE, 2107 data: true, 2108 }), 2109 }), 2110 DetectLocation: () => ({ 2111 id: "newtab-weather-menu-detect-my-location", 2112 action: actionCreators.AlsoToMain({ 2113 type: actionTypes.WEATHER_USER_OPT_IN_LOCATION, 2114 }), 2115 userEvent: "WEATHER_DETECT_LOCATION", 2116 }), 2117 ChangeWeatherDisplaySimple: () => ({ 2118 id: "newtab-weather-menu-change-weather-display-simple", 2119 action: actionCreators.OnlyToMain({ 2120 type: actionTypes.SET_PREF, 2121 data: { 2122 name: "weather.display", 2123 value: "simple", 2124 }, 2125 }), 2126 }), 2127 ChangeWeatherDisplayDetailed: () => ({ 2128 id: "newtab-weather-menu-change-weather-display-detailed", 2129 action: actionCreators.OnlyToMain({ 2130 type: actionTypes.SET_PREF, 2131 data: { 2132 name: "weather.display", 2133 value: "detailed", 2134 }, 2135 }), 2136 }), 2137 ChangeTempUnitFahrenheit: () => ({ 2138 id: "newtab-weather-menu-change-temperature-units-fahrenheit", 2139 action: actionCreators.OnlyToMain({ 2140 type: actionTypes.SET_PREF, 2141 data: { 2142 name: "weather.temperatureUnits", 2143 value: "f", 2144 }, 2145 }), 2146 }), 2147 ChangeTempUnitCelsius: () => ({ 2148 id: "newtab-weather-menu-change-temperature-units-celsius", 2149 action: actionCreators.OnlyToMain({ 2150 type: actionTypes.SET_PREF, 2151 data: { 2152 name: "weather.temperatureUnits", 2153 value: "c", 2154 }, 2155 }), 2156 }), 2157 HideWeather: () => ({ 2158 id: "newtab-weather-menu-hide-weather", 2159 action: actionCreators.OnlyToMain({ 2160 type: actionTypes.SET_PREF, 2161 data: { 2162 name: "showWeather", 2163 value: false, 2164 }, 2165 }), 2166 }), 2167 OpenLearnMoreURL: site => ({ 2168 id: "newtab-weather-menu-learn-more", 2169 action: actionCreators.OnlyToMain({ 2170 type: actionTypes.OPEN_LINK, 2171 data: { url: site.url }, 2172 }), 2173 }), 2174 SectionBlock: ({ 2175 sectionPersonalization, 2176 sectionKey, 2177 sectionPosition, 2178 title, 2179 }) => ({ 2180 id: "newtab-menu-section-block", 2181 icon: "delete", 2182 action: { 2183 // Open the confirmation dialog to block a section. 2184 type: actionTypes.DIALOG_OPEN, 2185 data: { 2186 onConfirm: [ 2187 // Once the user confirmed their intention to block this section, 2188 // update their preferences. 2189 actionCreators.AlsoToMain({ 2190 type: actionTypes.SECTION_PERSONALIZATION_SET, 2191 data: { 2192 ...sectionPersonalization, 2193 [sectionKey]: { 2194 isBlocked: true, 2195 isFollowed: false, 2196 }, 2197 }, 2198 }), 2199 // Telemetry 2200 actionCreators.OnlyToMain({ 2201 type: actionTypes.BLOCK_SECTION, 2202 data: { 2203 section: sectionKey, 2204 section_position: sectionPosition, 2205 event_source: "CONTEXT_MENU", 2206 }, 2207 }), 2208 // Also broadcast that this section has been blocked so that 2209 // the confirmation dialog knows it needs to disappear now. 2210 actionCreators.AlsoToMain({ 2211 type: actionTypes.DIALOG_CLOSE, 2212 }), 2213 ], 2214 // Pass Fluent strings to ConfirmDialog component for the copy 2215 // of the prompt to block sections. 2216 body_string_id: [ 2217 "newtab-section-confirm-block-topic-p1", 2218 "newtab-section-confirm-block-topic-p2", 2219 ], 2220 confirm_button_string_id: "newtab-section-block-topic-button", 2221 confirm_button_string_args: { topic: title }, 2222 cancel_button_string_id: "newtab-section-cancel-button", 2223 }, 2224 }, 2225 userEvent: "DIALOG_OPEN", 2226 }), 2227 SectionUnfollow: ({ 2228 sectionPersonalization, 2229 sectionKey, 2230 sectionPosition, 2231 }) => ({ 2232 id: "newtab-menu-section-unfollow", 2233 action: actionCreators.AlsoToMain({ 2234 type: actionTypes.SECTION_PERSONALIZATION_SET, 2235 data: (({ [sectionKey]: _sectionKey, ...remaining }) => remaining)( 2236 sectionPersonalization 2237 ), 2238 }), 2239 impression: actionCreators.OnlyToMain({ 2240 type: actionTypes.UNFOLLOW_SECTION, 2241 data: { 2242 section: sectionKey, 2243 section_position: sectionPosition, 2244 event_source: "CONTEXT_MENU", 2245 }, 2246 }), 2247 }), 2248 ManageSponsoredContent: () => ({ 2249 id: "newtab-menu-manage-sponsored-content", 2250 action: actionCreators.OnlyToMain({ type: actionTypes.SETTINGS_OPEN }), 2251 userEvent: "OPEN_NEWTAB_PREFS", 2252 }), 2253 OurSponsorsAndYourPrivacy: () => ({ 2254 id: "newtab-menu-our-sponsors-and-your-privacy", 2255 action: actionCreators.OnlyToMain({ 2256 type: actionTypes.OPEN_LINK, 2257 data: { 2258 url: "https://support.mozilla.org/kb/pocket-sponsored-stories-new-tabs", 2259 }, 2260 }), 2261 userEvent: "CLICK_PRIVACY_INFO", 2262 }), 2263 ReportAd: site => { 2264 return { 2265 id: "newtab-menu-report-this-ad", 2266 action: actionCreators.AlsoToMain({ 2267 type: actionTypes.REPORT_AD_OPEN, 2268 data: { 2269 card_type: site.card_type, 2270 position: site.position, 2271 reporting_url: site.shim.report, 2272 url: site.url, 2273 }, 2274 }), 2275 }; 2276 }, 2277 2278 ReportContent: site => { 2279 return { 2280 id: "newtab-menu-report", 2281 action: actionCreators.AlsoToMain({ 2282 type: actionTypes.REPORT_CONTENT_OPEN, 2283 data: { 2284 card_type: site.card_type, 2285 corpus_item_id: site.corpus_item_id, 2286 scheduled_corpus_item_id: site.scheduled_corpus_item_id, 2287 section_position: site.section_position, 2288 section: site.section, 2289 title: site.title, 2290 topic: site.topic, 2291 url: site.url, 2292 }, 2293 }), 2294 }; 2295 }, 2296 }; 2297 2298 ;// CONCATENATED MODULE: ./content-src/components/LinkMenu/LinkMenu.jsx 2299 /* This Source Code Form is subject to the terms of the Mozilla Public 2300 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 2301 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 2302 2303 2304 2305 2306 2307 2308 const DEFAULT_SITE_MENU_OPTIONS = ["CheckPinTopSite", "EditTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"]; 2309 class _LinkMenu extends (external_React_default()).PureComponent { 2310 getOptions() { 2311 const { 2312 props 2313 } = this; 2314 const { 2315 site, 2316 index, 2317 source, 2318 isPrivateBrowsingEnabled, 2319 siteInfo, 2320 platform, 2321 dispatch, 2322 options, 2323 shouldSendImpressionStats, 2324 userEvent = actionCreators.UserEvent 2325 } = props; 2326 2327 // Handle special case of default site 2328 const propOptions = site.isDefault && !site.searchTopSite && !site.sponsored_position ? DEFAULT_SITE_MENU_OPTIONS : options; 2329 const linkMenuOptions = propOptions.map(o => LinkMenuOptions[o](site, index, source, isPrivateBrowsingEnabled, siteInfo, platform)).map(option => { 2330 const { 2331 action, 2332 impression, 2333 id, 2334 type, 2335 userEvent: eventName 2336 } = option; 2337 if (!type && id) { 2338 option.onClick = (event = {}) => { 2339 const { 2340 ctrlKey, 2341 metaKey, 2342 shiftKey, 2343 button 2344 } = event; 2345 // Only send along event info if there's something non-default to send 2346 if (ctrlKey || metaKey || shiftKey || button === 1) { 2347 action.data = Object.assign({ 2348 event: { 2349 ctrlKey, 2350 metaKey, 2351 shiftKey, 2352 button 2353 } 2354 }, action.data); 2355 } 2356 dispatch(action); 2357 if (eventName) { 2358 let value; 2359 // Bug 1958135: Pass additional info to ac.OPEN_NEW_WINDOW event 2360 if (action.type === "OPEN_NEW_WINDOW") { 2361 const { 2362 card_type, 2363 corpus_item_id, 2364 event_source, 2365 fetchTimestamp, 2366 firstVisibleTimestamp, 2367 format, 2368 is_section_followed, 2369 received_rank, 2370 recommendation_id, 2371 recommended_at, 2372 scheduled_corpus_item_id, 2373 section_position, 2374 section, 2375 selected_topics, 2376 tile_id, 2377 topic 2378 } = action.data; 2379 value = { 2380 card_type, 2381 corpus_item_id, 2382 event_source, 2383 fetchTimestamp, 2384 firstVisibleTimestamp, 2385 format, 2386 received_rank, 2387 recommendation_id, 2388 recommended_at, 2389 scheduled_corpus_item_id, 2390 ...(section ? { 2391 is_section_followed, 2392 section_position, 2393 section 2394 } : {}), 2395 selected_topics: selected_topics ? selected_topics : "", 2396 tile_id, 2397 topic 2398 }; 2399 } else { 2400 value = { 2401 card_type: site.flight_id ? "spoc" : "organic" 2402 }; 2403 } 2404 const userEventData = Object.assign({ 2405 event: eventName, 2406 source, 2407 action_position: index, 2408 value 2409 }, siteInfo); 2410 dispatch(userEvent(userEventData)); 2411 if (impression && shouldSendImpressionStats) { 2412 dispatch(impression); 2413 } 2414 } 2415 }; 2416 } 2417 return option; 2418 }); 2419 2420 // This is for accessibility to support making each item tabbable. 2421 // We want to know which item is the first and which item 2422 // is the last, so we can close the context menu accordingly. 2423 linkMenuOptions[0].first = true; 2424 linkMenuOptions[linkMenuOptions.length - 1].last = true; 2425 return linkMenuOptions; 2426 } 2427 render() { 2428 return /*#__PURE__*/external_React_default().createElement(ContextMenu, { 2429 onUpdate: this.props.onUpdate, 2430 onShow: this.props.onShow, 2431 options: this.getOptions(), 2432 keyboardAccess: this.props.keyboardAccess 2433 }); 2434 } 2435 } 2436 const getState = state => ({ 2437 isPrivateBrowsingEnabled: state.Prefs.values.isPrivateBrowsingEnabled, 2438 platform: state.Prefs.values.platform 2439 }); 2440 const LinkMenu = (0,external_ReactRedux_namespaceObject.connect)(getState)(_LinkMenu); 2441 ;// CONCATENATED MODULE: ./content-src/components/ContextMenu/ContextMenuButton.jsx 2442 /* This Source Code Form is subject to the terms of the Mozilla Public 2443 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 2444 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 2445 2446 2447 class ContextMenuButton extends (external_React_default()).PureComponent { 2448 constructor(props) { 2449 super(props); 2450 this.state = { 2451 showContextMenu: false, 2452 contextMenuKeyboard: false 2453 }; 2454 this.onClick = this.onClick.bind(this); 2455 this.onKeyDown = this.onKeyDown.bind(this); 2456 this.onUpdate = this.onUpdate.bind(this); 2457 } 2458 openContextMenu(isKeyBoard) { 2459 if (this.props.onUpdate) { 2460 this.props.onUpdate(true); 2461 } 2462 this.setState({ 2463 showContextMenu: true, 2464 contextMenuKeyboard: isKeyBoard 2465 }); 2466 } 2467 onClick(event) { 2468 event.preventDefault(); 2469 this.openContextMenu(false, event); 2470 } 2471 onKeyDown(event) { 2472 if (event.key === "Enter" || event.key === " ") { 2473 event.preventDefault(); 2474 this.openContextMenu(true, event); 2475 } 2476 } 2477 onUpdate(showContextMenu) { 2478 if (this.props.onUpdate) { 2479 this.props.onUpdate(showContextMenu); 2480 } 2481 this.setState({ 2482 showContextMenu 2483 }); 2484 } 2485 render() { 2486 const { 2487 tooltipArgs, 2488 tooltip, 2489 children, 2490 refFunction 2491 } = this.props; 2492 const { 2493 showContextMenu, 2494 contextMenuKeyboard 2495 } = this.state; 2496 return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("button", { 2497 "aria-haspopup": "menu", 2498 "aria-expanded": showContextMenu, 2499 "data-l10n-id": tooltip, 2500 "data-l10n-args": tooltipArgs ? JSON.stringify(tooltipArgs) : null, 2501 className: "context-menu-button icon", 2502 onKeyDown: this.onKeyDown, 2503 onClick: this.onClick, 2504 ref: refFunction, 2505 tabIndex: this.props.tabIndex || 0, 2506 onFocus: this.props.onFocus 2507 }), showContextMenu ? /*#__PURE__*/external_React_default().cloneElement(children, { 2508 keyboardAccess: contextMenuKeyboard, 2509 onUpdate: this.onUpdate 2510 }) : null); 2511 } 2512 } 2513 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx 2514 /* This Source Code Form is subject to the terms of the Mozilla Public 2515 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 2516 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 2517 2518 2519 2520 2521 2522 2523 class _DSLinkMenu extends (external_React_default()).PureComponent { 2524 render() { 2525 const { 2526 index, 2527 dispatch 2528 } = this.props; 2529 let TOP_STORIES_CONTEXT_MENU_OPTIONS; 2530 const PREF_REPORT_ADS_ENABLED = "discoverystream.reportAds.enabled"; 2531 const prefs = this.props.Prefs.values; 2532 const showAdsReporting = prefs[PREF_REPORT_ADS_ENABLED]; 2533 const isSpoc = this.props.card_type === "spoc"; 2534 if (isSpoc) { 2535 TOP_STORIES_CONTEXT_MENU_OPTIONS = ["BlockUrl", ...(showAdsReporting ? ["ReportAd"] : []), "ManageSponsoredContent", "OurSponsorsAndYourPrivacy"]; 2536 } else { 2537 TOP_STORIES_CONTEXT_MENU_OPTIONS = ["CheckBookmark", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", ...(this.props.section ? ["ReportContent"] : [])]; 2538 } 2539 const type = this.props.type || "DISCOVERY_STREAM"; 2540 const title = this.props.title || this.props.source; 2541 return /*#__PURE__*/external_React_default().createElement("div", { 2542 className: "context-menu-position-container" 2543 }, /*#__PURE__*/external_React_default().createElement(ContextMenuButton, { 2544 tooltip: "newtab-menu-content-tooltip", 2545 tooltipArgs: { 2546 title 2547 }, 2548 onUpdate: this.props.onMenuUpdate, 2549 tabIndex: this.props.tabIndex 2550 }, /*#__PURE__*/external_React_default().createElement(LinkMenu, { 2551 dispatch: dispatch, 2552 index: index, 2553 source: type.toUpperCase(), 2554 onShow: this.props.onMenuShow, 2555 options: TOP_STORIES_CONTEXT_MENU_OPTIONS, 2556 shouldSendImpressionStats: true, 2557 userEvent: actionCreators.DiscoveryStreamUserEvent, 2558 site: { 2559 referrer: "https://getpocket.com/recommendations", 2560 title: this.props.title, 2561 type: this.props.type, 2562 url: this.props.url, 2563 guid: this.props.id, 2564 pocket_id: this.props.pocket_id, 2565 card_type: this.props.card_type, 2566 shim: this.props.shim, 2567 bookmarkGuid: this.props.bookmarkGuid, 2568 flight_id: this.props.flightId, 2569 tile_id: this.props.tile_id, 2570 recommendation_id: this.props.recommendation_id, 2571 corpus_item_id: this.props.corpus_item_id, 2572 scheduled_corpus_item_id: this.props.scheduled_corpus_item_id, 2573 firstVisibleTimestamp: this.props.firstVisibleTimestamp, 2574 recommended_at: this.props.recommended_at, 2575 received_rank: this.props.received_rank, 2576 topic: this.props.topic, 2577 position: index, 2578 ...(this.props.format ? { 2579 format: this.props.format 2580 } : {}), 2581 ...(this.props.section ? { 2582 section: this.props.section, 2583 section_position: this.props.section_position, 2584 is_section_followed: this.props.is_section_followed 2585 } : {}) 2586 } 2587 }))); 2588 } 2589 } 2590 const DSLinkMenu = (0,external_ReactRedux_namespaceObject.connect)(state => ({ 2591 Prefs: state.Prefs 2592 }))(_DSLinkMenu); 2593 ;// CONCATENATED MODULE: ./content-src/lib/utils.jsx 2594 /* This Source Code Form is subject to the terms of the Mozilla Public 2595 * License, v. 2.0. If a copy of the MPL was not distributed with this 2596 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 2597 2598 const PREF_WEATHER_PLACEMENT = "weather.placement"; 2599 const PREF_DAILY_BRIEF_SECTIONID = "discoverystream.dailyBrief.sectionId"; 2600 const PREF_DAILY_BRIEF_ENABLED = "discoverystream.dailyBrief.enabled"; 2601 const PREF_STORIES_ENABLED = "feeds.section.topstories"; 2602 const PREF_SYSTEM_STORIES_ENABLED = "feeds.system.topstories"; 2603 2604 /** 2605 * A custom react hook that sets up an IntersectionObserver to observe a single 2606 * or list of elements and triggers a callback when the element comes into the viewport 2607 * Note: The refs used should be an array type 2608 * 2609 * @function useIntersectionObserver 2610 * @param {function} callback - The function to call when an element comes into the viewport 2611 * @param {object} options - Options object passed to Intersection Observer: 2612 * https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver#options 2613 * @param {boolean} [isSingle = false] Boolean if the elements are an array or single element 2614 * 2615 * @returns {React.MutableRefObject} a ref containing an array of elements or single element 2616 */ 2617 function useIntersectionObserver(callback, threshold = 0.3) { 2618 const elementsRef = (0,external_React_namespaceObject.useRef)([]); 2619 const triggeredElements = (0,external_React_namespaceObject.useRef)(new WeakSet()); 2620 (0,external_React_namespaceObject.useEffect)(() => { 2621 const observer = new IntersectionObserver(entries => { 2622 entries.forEach(entry => { 2623 if (entry.isIntersecting && !triggeredElements.current.has(entry.target)) { 2624 triggeredElements.current.add(entry.target); 2625 callback(entry.target); 2626 observer.unobserve(entry.target); 2627 } 2628 }); 2629 }, { 2630 threshold 2631 }); 2632 elementsRef.current.forEach(el => { 2633 if (el && !triggeredElements.current.has(el)) { 2634 observer.observe(el); 2635 } 2636 }); 2637 2638 // Cleanup function to disconnect observer on unmount 2639 return () => observer.disconnect(); 2640 }, [callback, threshold]); 2641 return elementsRef; 2642 } 2643 2644 /** 2645 * Determines which column layout is active based on the screen width 2646 * 2647 * @param {number} screenWidth - The current window width (in pixels) 2648 * @returns {string} The active column layout (e.g. "col-3", "col-2", "col-1") 2649 */ 2650 function getActiveColumnLayout(screenWidth) { 2651 const breakpoints = [{ 2652 min: 1374, 2653 column: "col-4" 2654 }, 2655 // $break-point-sections-variant 2656 { 2657 min: 1122, 2658 column: "col-3" 2659 }, 2660 // $break-point-widest 2661 { 2662 min: 724, 2663 column: "col-2" 2664 }, 2665 // $break-point-layout-variant 2666 { 2667 min: 0, 2668 column: "col-1" 2669 } // (default layout) 2670 ]; 2671 return breakpoints.find(bp => screenWidth >= bp.min).column; 2672 } 2673 2674 /** 2675 * Determines the active card size ("small", "medium", or "large") based on the screen width 2676 * and class names applied to the card element at the time of an event (example: click) 2677 * 2678 * @param {number} screenWidth - The current window width (in pixels). 2679 * @param {string | string[]} classNames - A string or array of class names applied to the sections card. 2680 * @param {boolean[]} sectionsEnabled - If sections is not enabled, all cards are `medium-card` 2681 * @param {number} flightId - Error ege case: This function should not be called on spocs, which have flightId 2682 * @returns {"small-card" | "medium-card" | "large-card" | null} The active card type, or null if none is matched. 2683 */ 2684 function getActiveCardSize(screenWidth, classNames, sectionsEnabled, flightId) { 2685 // Only applies to sponsored content 2686 if (flightId) { 2687 return "spoc"; 2688 } 2689 2690 // Default layout only supports `medium-card` 2691 if (!sectionsEnabled) { 2692 // Missing arguments 2693 return "medium-card"; 2694 } 2695 2696 // Return null if no values are available 2697 if (!screenWidth || !classNames) { 2698 // Missing arguments 2699 return null; 2700 } 2701 const classList = classNames.split(" "); 2702 const cardTypes = ["small", "medium", "large"]; 2703 2704 // Determine which column is active based on the current screen width 2705 const currColumnCount = getActiveColumnLayout(screenWidth); 2706 2707 // Match the card type for that column count 2708 for (let type of cardTypes) { 2709 const className = `${currColumnCount}-${type}`; 2710 if (classList.includes(className)) { 2711 // Special case: below $break-point-medium (610px), report `col-1-small` as medium 2712 if (screenWidth < 610 && currColumnCount === "col-1" && type === "small") { 2713 return "medium-card"; 2714 } 2715 // Will be either "small-card", "medium-card", or "large-card" 2716 return `${type}-card`; 2717 } 2718 } 2719 return null; 2720 } 2721 const CONFETTI_VARS = ["--color-red-40", "--color-yellow-40", "--color-purple-40", "--color-blue-40", "--color-green-40"]; 2722 2723 /** 2724 * Custom hook to animate a confetti burst. 2725 * 2726 * @param {number} count Number of particles 2727 * @param {number} spread spread of confetti 2728 * @returns {[React.RefObject<HTMLCanvasElement>, () => void]} 2729 */ 2730 function useConfetti(count = 80, spread = Math.PI / 3) { 2731 // avoid errors from about:home cache 2732 const prefersReducedMotion = typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches; 2733 let colors; 2734 // if in abouthome cache, getComputedStyle will not be available 2735 if (typeof getComputedStyle === "function") { 2736 const styles = getComputedStyle(document.documentElement); 2737 colors = CONFETTI_VARS.map(variable => styles.getPropertyValue(variable).trim()); 2738 } else { 2739 colors = ["#fa5e75", "#de9600", "#c671eb", "#3f94ff", "#37b847"]; 2740 } 2741 const canvasRef = (0,external_React_namespaceObject.useRef)(null); 2742 const particlesRef = (0,external_React_namespaceObject.useRef)([]); 2743 const animationFrameRef = (0,external_React_namespaceObject.useRef)(0); 2744 2745 // initialize/reset pool 2746 const initializeConfetti = (0,external_React_namespaceObject.useCallback)((width, height) => { 2747 const centerX = width / 2; 2748 const centerY = height; 2749 const pool = particlesRef.current; 2750 2751 // Create or overwrite each particle’s initial state 2752 for (let i = 0; i < count; i++) { 2753 const angle = Math.PI / 2 + (Math.random() - 0.5) * spread; 2754 const cos = Math.cos(angle); 2755 const sin = Math.sin(angle); 2756 const color = colors[Math.floor(Math.random() * colors.length)]; 2757 pool[i] = { 2758 x: centerX + (Math.random() - 0.5) * 40, 2759 y: centerY, 2760 cos, 2761 sin, 2762 velocity: Math.random() * 6 + 6, 2763 gravity: 0.3, 2764 decay: 0.96, 2765 size: 8, 2766 color, 2767 life: 0, 2768 maxLife: 100, 2769 tilt: Math.random() * Math.PI * 2, 2770 tiltSpeed: Math.random() * 0.2 + 0.05 2771 }; 2772 } 2773 }, [count, spread, colors]); 2774 2775 // Core animation loop — updates physics & renders each frame 2776 const animateParticles = (0,external_React_namespaceObject.useCallback)(canvas => { 2777 const context = canvas.getContext("2d"); 2778 const { 2779 width, 2780 height 2781 } = canvas; 2782 const pool = particlesRef.current; 2783 2784 // Clear the entire canvas each frame 2785 context.clearRect(0, 0, width, height); 2786 let anyAlive = false; 2787 for (let particle of pool) { 2788 if (particle.life < particle.maxLife) { 2789 anyAlive = true; 2790 2791 // update each particles physics: position, velocity decay, gravity, tilt, lifespan 2792 particle.velocity *= particle.decay; 2793 particle.x += particle.cos * particle.velocity; 2794 particle.y -= particle.sin * particle.velocity; 2795 particle.y += particle.gravity; 2796 particle.tilt += particle.tiltSpeed; 2797 particle.life += 1; 2798 } 2799 2800 // Draw: apply alpha, transform & draw a rotated, scaled square 2801 const alphaValue = 1 - particle.life / particle.maxLife; 2802 const scaleY = Math.sin(particle.tilt); 2803 context.globalAlpha = alphaValue; 2804 context.setTransform(1, 0, 0, 1, particle.x, particle.y); 2805 context.rotate(Math.PI / 4); 2806 context.scale(1, scaleY); 2807 context.fillStyle = particle.color; 2808 context.fillRect(-particle.size / 2, -particle.size / 2, particle.size, particle.size); 2809 2810 // reset each particle 2811 context.setTransform(1, 0, 0, 1, 0, 0); 2812 context.globalAlpha = 1; 2813 } 2814 if (anyAlive) { 2815 // continue the animation 2816 animationFrameRef.current = requestAnimationFrame(() => { 2817 animateParticles(canvas); 2818 }); 2819 } else { 2820 cancelAnimationFrame(animationFrameRef.current); 2821 context.clearRect(0, 0, width, height); 2822 } 2823 }, []); 2824 2825 // Resets and starts a new confetti animation 2826 const fireConfetti = (0,external_React_namespaceObject.useCallback)(() => { 2827 if (prefersReducedMotion) { 2828 return; 2829 } 2830 const canvas = canvasRef?.current; 2831 if (canvas) { 2832 cancelAnimationFrame(animationFrameRef.current); 2833 initializeConfetti(canvas.width, canvas.height); 2834 animateParticles(canvas); 2835 } 2836 }, [initializeConfetti, animateParticles, prefersReducedMotion]); 2837 return [canvasRef, fireConfetti]; 2838 } 2839 function selectWeatherPlacement(state) { 2840 const prefs = state.Prefs.values || {}; 2841 2842 // Intent: only placed in section if explicitly requested 2843 const placementPref = prefs.trainhopConfig?.dailyBriefing?.placement || prefs[PREF_WEATHER_PLACEMENT]; 2844 if (placementPref === "header" || !placementPref) { 2845 return "header"; 2846 } 2847 const sections = state.DiscoveryStream.feeds.data["https://merino.services.mozilla.com/api/v1/curated-recommendations"]?.data.sections ?? []; 2848 // check the following prefs to make sure weather is elligible to be placed in sections 2849 // 1. The daily brieifng section must be availible and in the top position 2850 // 2. That the daily briefing section has not been blocked 2851 // 3. That reccomended stories are truned on 2852 // Otherwise it should be placed in the header 2853 const pocketEnabled = prefs[PREF_STORIES_ENABLED] && prefs[PREF_SYSTEM_STORIES_ENABLED]; 2854 const sectionPersonalization = state.DiscoveryStream?.sectionPersonalization || {}; 2855 const dailyBriefEnabled = prefs.trainhopConfig?.dailyBriefing?.enabled || prefs[PREF_DAILY_BRIEF_ENABLED]; 2856 const sectionId = prefs.trainhopConfig?.dailyBriefing?.sectionId || prefs[PREF_DAILY_BRIEF_SECTIONID]; 2857 const notBlocked = sectionId && !sectionPersonalization[sectionId]?.isBlocked; 2858 let filteredSections = sections.filter(section => !sectionPersonalization[section.sectionKey]?.isBlocked); 2859 const foundSection = filteredSections.find(section => section.sectionKey === sectionId); 2860 const isTopSection = foundSection?.receivedRank === 0 || filteredSections.indexOf(foundSection) === 0; 2861 const eligible = pocketEnabled && dailyBriefEnabled && sectionId && notBlocked && isTopSection; 2862 return eligible ? "section" : "header"; 2863 } 2864 2865 ;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSitesConstants.mjs 2866 /* This Source Code Form is subject to the terms of the Mozilla Public 2867 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 2868 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 2869 2870 const TOP_SITES_SOURCE = "TOP_SITES"; 2871 const TOP_SITES_CONTEXT_MENU_OPTIONS = [ 2872 "CheckPinTopSite", 2873 "EditTopSite", 2874 "Separator", 2875 "OpenInNewWindow", 2876 "OpenInPrivateWindow", 2877 "Separator", 2878 "BlockUrl", 2879 "DeleteUrl", 2880 ]; 2881 const TOP_SITES_SPOC_CONTEXT_MENU_OPTIONS = [ 2882 "OpenInNewWindow", 2883 "OpenInPrivateWindow", 2884 "Separator", 2885 "BlockUrl", 2886 "ShowPrivacyInfo", 2887 ]; 2888 const TOP_SITES_SPONSORED_POSITION_CONTEXT_MENU_OPTIONS = [ 2889 "OpenInNewWindow", 2890 "OpenInPrivateWindow", 2891 "Separator", 2892 "BlockUrl", 2893 "AboutSponsored", 2894 ]; 2895 // the special top site for search shortcut experiment can only have the option to unpin (which removes) the topsite 2896 const TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS = [ 2897 "CheckPinTopSite", 2898 "Separator", 2899 "BlockUrl", 2900 ]; 2901 // minimum size necessary to show a rich icon instead of a screenshot 2902 const MIN_RICH_FAVICON_SIZE = 96; 2903 // minimum size necessary to show any icon 2904 const MIN_SMALL_FAVICON_SIZE = 16; 2905 2906 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx 2907 /* This Source Code Form is subject to the terms of the Mozilla Public 2908 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 2909 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 2910 2911 2912 2913 2914 2915 const VISIBLE = "visible"; 2916 const VISIBILITY_CHANGE_EVENT = "visibilitychange"; 2917 2918 // Per analytical requirement, we set the minimal intersection ratio to 2919 // 0.5, and an impression is identified when the wrapped item has at least 2920 // 50% visibility. 2921 // 2922 // This constant is exported for unit test 2923 const INTERSECTION_RATIO = 0.5; 2924 2925 /** 2926 * Impression wrapper for Discovery Stream related React components. 2927 * 2928 * It makes use of the Intersection Observer API to detect the visibility, 2929 * and relies on page visibility to ensure the impression is reported 2930 * only when the component is visible on the page. 2931 * 2932 * Note: 2933 * * This wrapper used to be used either at the individual card level, 2934 * or by the card container components. 2935 * It is now only used for individual card level. 2936 * * Each impression will be sent only once as soon as the desired 2937 * visibility is detected 2938 * * Batching is not yet implemented, hence it might send multiple 2939 * impression pings separately 2940 */ 2941 class ImpressionStats_ImpressionStats extends (external_React_default()).PureComponent { 2942 // This checks if the given cards are the same as those in the last impression ping. 2943 // If so, it should not send the same impression ping again. 2944 _needsImpressionStats(cards) { 2945 if (!this.impressionCardGuids || this.impressionCardGuids.length !== cards.length) { 2946 return true; 2947 } 2948 for (let i = 0; i < cards.length; i++) { 2949 if (cards[i].id !== this.impressionCardGuids[i]) { 2950 return true; 2951 } 2952 } 2953 return false; 2954 } 2955 _dispatchImpressionStats() { 2956 const { 2957 props 2958 } = this; 2959 const cards = props.rows; 2960 if (this.props.flightId) { 2961 this.props.dispatch(actionCreators.OnlyToMain({ 2962 type: actionTypes.DISCOVERY_STREAM_SPOC_IMPRESSION, 2963 data: { 2964 flightId: this.props.flightId 2965 } 2966 })); 2967 2968 // Record sponsored topsites impressions if the source is `TOP_SITES_SOURCE`. 2969 if (this.props.source === TOP_SITES_SOURCE) { 2970 for (const card of cards) { 2971 this.props.dispatch(actionCreators.OnlyToMain({ 2972 type: actionTypes.TOP_SITES_SPONSORED_IMPRESSION_STATS, 2973 data: { 2974 type: "impression", 2975 tile_id: card.id, 2976 source: "newtab", 2977 advertiser: card.advertiser, 2978 // Keep the 0-based position, can be adjusted by the telemetry 2979 // sender if necessary. 2980 position: card.pos, 2981 attribution: card.attribution 2982 } 2983 })); 2984 } 2985 } 2986 } 2987 if (this._needsImpressionStats(cards)) { 2988 props.dispatch(actionCreators.DiscoveryStreamImpressionStats({ 2989 source: props.source.toUpperCase(), 2990 window_inner_width: window.innerWidth, 2991 window_inner_height: window.innerHeight, 2992 tiles: cards.map(link => ({ 2993 id: link.id, 2994 pos: link.pos, 2995 type: props.flightId ? "spoc" : "organic", 2996 ...(link.shim ? { 2997 shim: link.shim 2998 } : {}), 2999 recommendation_id: link.recommendation_id, 3000 fetchTimestamp: link.fetchTimestamp, 3001 corpus_item_id: link.corpus_item_id, 3002 scheduled_corpus_item_id: link.scheduled_corpus_item_id, 3003 recommended_at: link.recommended_at, 3004 received_rank: link.received_rank, 3005 topic: link.topic, 3006 features: link.features, 3007 attribution: link.attribution, 3008 ...(link.format ? { 3009 format: link.format 3010 } : { 3011 format: getActiveCardSize(window.innerWidth, link.class_names, link.section, link.flightId) 3012 }), 3013 ...(link.section ? { 3014 section: link.section, 3015 section_position: link.section_position, 3016 is_section_followed: link.is_section_followed, 3017 layout_name: link.sectionLayoutName 3018 } : {}) 3019 })), 3020 firstVisibleTimestamp: props.firstVisibleTimestamp 3021 })); 3022 this.impressionCardGuids = cards.map(link => link.id); 3023 } 3024 } 3025 3026 // This checks if the given cards are the same as those in the last loaded content ping. 3027 // If so, it should not send the same loaded content ping again. 3028 _needsLoadedContent(cards) { 3029 if (!this.loadedContentGuids || this.loadedContentGuids.length !== cards.length) { 3030 return true; 3031 } 3032 for (let i = 0; i < cards.length; i++) { 3033 if (cards[i].id !== this.loadedContentGuids[i]) { 3034 return true; 3035 } 3036 } 3037 return false; 3038 } 3039 _dispatchLoadedContent() { 3040 const { 3041 props 3042 } = this; 3043 const cards = props.rows; 3044 if (this._needsLoadedContent(cards)) { 3045 props.dispatch(actionCreators.DiscoveryStreamLoadedContent({ 3046 source: props.source.toUpperCase(), 3047 tiles: cards.map(link => ({ 3048 id: link.id, 3049 pos: link.pos 3050 })) 3051 })); 3052 this.loadedContentGuids = cards.map(link => link.id); 3053 } 3054 } 3055 setImpressionObserverOrAddListener() { 3056 const { 3057 props 3058 } = this; 3059 if (!props.dispatch) { 3060 return; 3061 } 3062 if (props.document.visibilityState === VISIBLE) { 3063 // Send the loaded content ping once the page is visible. 3064 this._dispatchLoadedContent(); 3065 this.setImpressionObserver(); 3066 } else { 3067 // We should only ever send the latest impression stats ping, so remove any 3068 // older listeners. 3069 if (this._onVisibilityChange) { 3070 props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 3071 } 3072 this._onVisibilityChange = () => { 3073 if (props.document.visibilityState === VISIBLE) { 3074 // Send the loaded content ping once the page is visible. 3075 this._dispatchLoadedContent(); 3076 this.setImpressionObserver(); 3077 props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 3078 } 3079 }; 3080 props.document.addEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 3081 } 3082 } 3083 3084 /** 3085 * Set an impression observer for the wrapped component. It makes use of 3086 * the Intersection Observer API to detect if the wrapped component is 3087 * visible with a desired ratio, and only sends impression if that's the case. 3088 * 3089 * See more details about Intersection Observer API at: 3090 * https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API 3091 */ 3092 setImpressionObserver() { 3093 const { 3094 props 3095 } = this; 3096 if (!props.rows.length) { 3097 return; 3098 } 3099 this._handleIntersect = entries => { 3100 if (entries.some(entry => entry.isIntersecting && entry.intersectionRatio >= INTERSECTION_RATIO)) { 3101 this._dispatchImpressionStats(); 3102 this.impressionObserver.unobserve(this.refs.impression); 3103 } 3104 }; 3105 const options = { 3106 threshold: INTERSECTION_RATIO 3107 }; 3108 this.impressionObserver = new props.IntersectionObserver(this._handleIntersect, options); 3109 this.impressionObserver.observe(this.refs.impression); 3110 } 3111 componentDidMount() { 3112 if (this.props.rows.length) { 3113 this.setImpressionObserverOrAddListener(); 3114 } 3115 } 3116 componentWillUnmount() { 3117 if (this._handleIntersect && this.impressionObserver) { 3118 this.impressionObserver.unobserve(this.refs.impression); 3119 } 3120 if (this._onVisibilityChange) { 3121 this.props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 3122 } 3123 } 3124 render() { 3125 return /*#__PURE__*/external_React_default().createElement("div", { 3126 ref: "impression", 3127 className: "impression-observer" 3128 }, this.props.children); 3129 } 3130 } 3131 ImpressionStats_ImpressionStats.defaultProps = { 3132 IntersectionObserver: globalThis.IntersectionObserver, 3133 document: globalThis.document, 3134 rows: [], 3135 source: "" 3136 }; 3137 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx 3138 function SafeAnchor_extends() { return SafeAnchor_extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, SafeAnchor_extends.apply(null, arguments); } 3139 /* This Source Code Form is subject to the terms of the Mozilla Public 3140 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3141 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 3142 3143 3144 3145 class SafeAnchor extends (external_React_default()).PureComponent { 3146 constructor(props) { 3147 super(props); 3148 this.onClick = this.onClick.bind(this); 3149 } 3150 onClick(event) { 3151 // Use dispatch instead of normal link click behavior to include referrer 3152 if (this.props.dispatch) { 3153 event.preventDefault(); 3154 const { 3155 altKey, 3156 button, 3157 ctrlKey, 3158 metaKey, 3159 shiftKey 3160 } = event; 3161 this.props.dispatch(actionCreators.OnlyToMain({ 3162 type: actionTypes.OPEN_LINK, 3163 data: { 3164 event: { 3165 altKey, 3166 button, 3167 ctrlKey, 3168 metaKey, 3169 shiftKey 3170 }, 3171 referrer: this.props.referrer || "https://getpocket.com/recommendations", 3172 // Use the anchor's url, which could have been cleaned up 3173 url: event.currentTarget.href, 3174 is_sponsored: this.props.isSponsored 3175 } 3176 })); 3177 } 3178 3179 // Propagate event if there's a handler 3180 if (this.props.onLinkClick) { 3181 this.props.onLinkClick(event); 3182 } 3183 } 3184 safeURI(url) { 3185 let protocol = null; 3186 try { 3187 protocol = new URL(url).protocol; 3188 } catch (e) { 3189 return ""; 3190 } 3191 const isAllowed = ["http:", "https:"].includes(protocol); 3192 if (!isAllowed) { 3193 console.warn(`${url} is not allowed for anchor targets.`); // eslint-disable-line no-console 3194 return ""; 3195 } 3196 return url; 3197 } 3198 render() { 3199 const { 3200 url, 3201 className, 3202 title, 3203 isSponsored, 3204 onFocus 3205 } = this.props; 3206 let anchor = /*#__PURE__*/external_React_default().createElement("a", SafeAnchor_extends({ 3207 href: this.safeURI(url), 3208 title: title, 3209 className: className, 3210 onClick: this.onClick, 3211 "data-is-sponsored-link": !!isSponsored 3212 }, this.props.tabIndex === 0 || this.props.tabIndex ? { 3213 ref: this.props.setRef, 3214 tabIndex: this.props.tabIndex 3215 } : {}, onFocus ? { 3216 onFocus 3217 } : {}), this.props.children); 3218 return anchor; 3219 } 3220 } 3221 ;// CONCATENATED MODULE: ./content-src/components/Card/types.mjs 3222 /* This Source Code Form is subject to the terms of the Mozilla Public 3223 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3224 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 3225 3226 const cardContextTypes = { 3227 history: { 3228 fluentID: "newtab-label-visited", 3229 icon: "history-item", 3230 }, 3231 removedBookmark: { 3232 fluentID: "newtab-label-removed-bookmark", 3233 icon: "bookmark-removed", 3234 }, 3235 bookmark: { 3236 fluentID: "newtab-label-bookmarked", 3237 icon: "bookmark-added", 3238 }, 3239 trending: { 3240 fluentID: "newtab-label-recommended", 3241 icon: "trending", 3242 }, 3243 pocket: { 3244 fluentID: "newtab-label-saved", 3245 icon: "pocket", 3246 }, 3247 download: { 3248 fluentID: "newtab-label-download", 3249 icon: "download", 3250 }, 3251 }; 3252 3253 ;// CONCATENATED MODULE: external "ReactTransitionGroup" 3254 const external_ReactTransitionGroup_namespaceObject = ReactTransitionGroup; 3255 ;// CONCATENATED MODULE: ./content-src/components/FluentOrText/FluentOrText.jsx 3256 /* This Source Code Form is subject to the terms of the Mozilla Public 3257 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3258 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 3259 3260 3261 3262 /** 3263 * Set text on a child element/component depending on if the message is already 3264 * translated plain text or a fluent id with optional args. 3265 */ 3266 class FluentOrText extends (external_React_default()).PureComponent { 3267 render() { 3268 // Ensure we have a single child to attach attributes 3269 const { 3270 children, 3271 message 3272 } = this.props; 3273 const child = children ? external_React_default().Children.only(children) : /*#__PURE__*/external_React_default().createElement("span", null); 3274 3275 // For a string message, just use it as the child's text 3276 let grandChildren = message; 3277 let extraProps; 3278 3279 // Convert a message object to set desired fluent-dom attributes 3280 if (typeof message === "object") { 3281 const args = message.args || message.values; 3282 extraProps = { 3283 "data-l10n-args": args && JSON.stringify(args), 3284 "data-l10n-id": message.id || message.string_id 3285 }; 3286 3287 // Use original children potentially with data-l10n-name attributes 3288 grandChildren = child.props.children; 3289 } 3290 3291 // Add the message to the child via fluent attributes or text node 3292 return /*#__PURE__*/external_React_default().cloneElement(child, extraProps, grandChildren); 3293 } 3294 } 3295 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx 3296 /* This Source Code Form is subject to the terms of the Mozilla Public 3297 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3298 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 3299 3300 3301 // eslint-disable-next-line no-shadow 3302 3303 3304 3305 3306 // Animation time is mirrored in DSContextFooter.scss 3307 const ANIMATION_DURATION = 3000; 3308 const DSMessageLabel = props => { 3309 const { 3310 context, 3311 context_type, 3312 mayHaveSectionsCards 3313 } = props; 3314 const { 3315 icon, 3316 fluentID 3317 } = cardContextTypes[context_type] || {}; 3318 if (!context && context_type && !mayHaveSectionsCards) { 3319 return /*#__PURE__*/external_React_default().createElement(external_ReactTransitionGroup_namespaceObject.TransitionGroup, { 3320 component: null 3321 }, /*#__PURE__*/external_React_default().createElement(external_ReactTransitionGroup_namespaceObject.CSSTransition, { 3322 key: fluentID, 3323 timeout: ANIMATION_DURATION, 3324 classNames: "story-animate" 3325 }, /*#__PURE__*/external_React_default().createElement(StatusMessage, { 3326 icon: icon, 3327 fluentID: fluentID 3328 }))); 3329 } 3330 return null; 3331 }; 3332 const StatusMessage = ({ 3333 icon, 3334 fluentID 3335 }) => /*#__PURE__*/external_React_default().createElement("div", { 3336 className: "status-message" 3337 }, /*#__PURE__*/external_React_default().createElement("span", { 3338 "aria-haspopup": "true", 3339 className: `story-badge-icon icon icon-${icon}` 3340 }), /*#__PURE__*/external_React_default().createElement("div", { 3341 className: "story-context-label", 3342 "data-l10n-id": fluentID 3343 })); 3344 const SponsorLabel = ({ 3345 sponsored_by_override, 3346 sponsor, 3347 context, 3348 newSponsoredLabel 3349 }) => { 3350 const classList = `story-sponsored-label ${newSponsoredLabel || ""} clamp`; 3351 // If override is not false or an empty string. 3352 if (sponsored_by_override) { 3353 return /*#__PURE__*/external_React_default().createElement("p", { 3354 className: classList 3355 }, sponsored_by_override); 3356 } else if (sponsored_by_override === "") { 3357 // We specifically want to display nothing if the server returns an empty string. 3358 // So the server can turn off the label. 3359 // This is to support the use cases where the sponsored context is displayed elsewhere. 3360 return null; 3361 } else if (sponsor) { 3362 return /*#__PURE__*/external_React_default().createElement("p", { 3363 className: classList 3364 }, /*#__PURE__*/external_React_default().createElement(FluentOrText, { 3365 message: { 3366 id: `newtab-label-sponsored-by`, 3367 values: { 3368 sponsor 3369 } 3370 } 3371 })); 3372 } else if (context) { 3373 return /*#__PURE__*/external_React_default().createElement("p", { 3374 className: classList 3375 }, context); 3376 } 3377 return null; 3378 }; 3379 class DSContextFooter extends (external_React_default()).PureComponent { 3380 render() { 3381 const { 3382 context, 3383 context_type, 3384 sponsor, 3385 sponsored_by_override, 3386 cta_button_variant, 3387 source, 3388 mayHaveSectionsCards 3389 } = this.props; 3390 const sponsorLabel = SponsorLabel({ 3391 sponsored_by_override, 3392 sponsor, 3393 context 3394 }); 3395 const dsMessageLabel = DSMessageLabel({ 3396 context, 3397 context_type, 3398 mayHaveSectionsCards 3399 }); 3400 if (cta_button_variant === "variant-a") { 3401 return /*#__PURE__*/external_React_default().createElement("div", { 3402 className: "story-footer" 3403 }, /*#__PURE__*/external_React_default().createElement("button", { 3404 "aria-hidden": "true", 3405 className: "story-cta-button" 3406 }, "Shop Now"), sponsorLabel); 3407 } 3408 if (cta_button_variant === "variant-b") { 3409 return /*#__PURE__*/external_React_default().createElement("div", { 3410 className: "story-footer" 3411 }, sponsorLabel, /*#__PURE__*/external_React_default().createElement("span", { 3412 className: "source clamp cta-footer-source" 3413 }, source)); 3414 } 3415 if (sponsorLabel || dsMessageLabel && context_type !== "pocket") { 3416 return /*#__PURE__*/external_React_default().createElement("div", { 3417 className: "story-footer" 3418 }, sponsorLabel, dsMessageLabel); 3419 } 3420 return null; 3421 } 3422 } 3423 const DSMessageFooter = props => { 3424 const { 3425 context, 3426 context_type 3427 } = props; 3428 const dsMessageLabel = DSMessageLabel({ 3429 context, 3430 context_type 3431 }); 3432 3433 // This case is specific and already displayed to the user elsewhere. 3434 if (!dsMessageLabel) { 3435 return null; 3436 } 3437 return /*#__PURE__*/external_React_default().createElement("div", { 3438 className: "story-footer" 3439 }, dsMessageLabel); 3440 }; 3441 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx 3442 /* This Source Code Form is subject to the terms of the Mozilla Public 3443 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3444 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 3445 3446 3447 3448 3449 3450 3451 3452 3453 3454 3455 3456 const READING_WPM = 220; 3457 const PREF_OHTTP_MERINO = "discoverystream.merino-provider.ohttp.enabled"; 3458 const PREF_OHTTP_UNIFIED_ADS = "unifiedAds.ohttp.enabled"; 3459 const DSCard_PREF_SECTIONS_ENABLED = "discoverystream.sections.enabled"; 3460 const PREF_FAVICONS_ENABLED = "discoverystream.publisherFavicon.enabled"; 3461 3462 /** 3463 * READ TIME FROM WORD COUNT 3464 * 3465 * @param {int} wordCount number of words in an article 3466 * @returns {int} number of words per minute in minutes 3467 */ 3468 function readTimeFromWordCount(wordCount) { 3469 if (!wordCount) { 3470 return false; 3471 } 3472 return Math.ceil(parseInt(wordCount, 10) / READING_WPM); 3473 } 3474 const DSSource = ({ 3475 source, 3476 timeToRead, 3477 newSponsoredLabel, 3478 context, 3479 sponsor, 3480 sponsored_by_override, 3481 icon_src, 3482 refinedCardsLayout 3483 }) => { 3484 // refinedCard styles will have a larger favicon size 3485 const faviconSize = refinedCardsLayout ? 20 : 16; 3486 3487 // First try to display sponsored label or time to read here. 3488 if (newSponsoredLabel) { 3489 // If we can display something for spocs, do so. 3490 if (sponsored_by_override || sponsor || context) { 3491 return /*#__PURE__*/external_React_default().createElement(SponsorLabel, { 3492 context: context, 3493 sponsor: sponsor, 3494 sponsored_by_override: sponsored_by_override, 3495 newSponsoredLabel: "new-sponsored-label" 3496 }); 3497 } 3498 } 3499 3500 // If we are not a spoc, and can display a time to read value. 3501 if (source && timeToRead) { 3502 return /*#__PURE__*/external_React_default().createElement("p", { 3503 className: "source clamp time-to-read" 3504 }, /*#__PURE__*/external_React_default().createElement(FluentOrText, { 3505 message: { 3506 id: `newtab-label-source-read-time`, 3507 values: { 3508 source, 3509 timeToRead 3510 } 3511 } 3512 })); 3513 } 3514 3515 // Otherwise display a default source. 3516 return /*#__PURE__*/external_React_default().createElement("div", { 3517 className: "source-wrapper" 3518 }, icon_src && /*#__PURE__*/external_React_default().createElement("img", { 3519 src: icon_src, 3520 height: faviconSize, 3521 width: faviconSize, 3522 alt: "" 3523 }), /*#__PURE__*/external_React_default().createElement("p", { 3524 className: "source clamp" 3525 }, source)); 3526 }; 3527 const DefaultMeta = ({ 3528 source, 3529 title, 3530 excerpt, 3531 timeToRead, 3532 newSponsoredLabel, 3533 context, 3534 context_type, 3535 sponsor, 3536 sponsored_by_override, 3537 ctaButtonVariant, 3538 dispatch, 3539 mayHaveSectionsCards, 3540 format, 3541 topic, 3542 isSectionsCard, 3543 showTopics, 3544 icon_src, 3545 refinedCardsLayout 3546 }) => { 3547 const shouldHaveFooterSection = isSectionsCard && showTopics; 3548 return /*#__PURE__*/external_React_default().createElement("div", { 3549 className: "meta" 3550 }, /*#__PURE__*/external_React_default().createElement("div", { 3551 className: "info-wrap" 3552 }, ctaButtonVariant !== "variant-b" && format !== "rectangle" && !refinedCardsLayout && /*#__PURE__*/external_React_default().createElement(DSSource, { 3553 source: source, 3554 timeToRead: timeToRead, 3555 newSponsoredLabel: newSponsoredLabel, 3556 context: context, 3557 sponsor: sponsor, 3558 sponsored_by_override: sponsored_by_override, 3559 icon_src: icon_src 3560 }), /*#__PURE__*/external_React_default().createElement("h3", { 3561 className: "title clamp" 3562 }, format === "rectangle" ? "Sponsored" : title), format === "rectangle" ? /*#__PURE__*/external_React_default().createElement("p", { 3563 className: "excerpt clamp" 3564 }, "Sponsored content supports our mission to build a better web.") : excerpt && /*#__PURE__*/external_React_default().createElement("p", { 3565 className: "excerpt clamp" 3566 }, excerpt)), (shouldHaveFooterSection || refinedCardsLayout) && /*#__PURE__*/external_React_default().createElement("div", { 3567 className: "sections-card-footer" 3568 }, refinedCardsLayout && format !== "rectangle" && format !== "spoc" && /*#__PURE__*/external_React_default().createElement(DSSource, { 3569 source: source, 3570 timeToRead: timeToRead, 3571 newSponsoredLabel: newSponsoredLabel, 3572 context: context, 3573 sponsor: sponsor, 3574 sponsored_by_override: sponsored_by_override, 3575 icon_src: icon_src, 3576 refinedCardsLayout: refinedCardsLayout 3577 }), showTopics && /*#__PURE__*/external_React_default().createElement("span", { 3578 className: "ds-card-topic", 3579 "data-l10n-id": `newtab-topic-label-${topic}` 3580 })), !newSponsoredLabel && /*#__PURE__*/external_React_default().createElement(DSContextFooter, { 3581 context_type: context_type, 3582 context: context, 3583 sponsor: sponsor, 3584 sponsored_by_override: sponsored_by_override, 3585 cta_button_variant: ctaButtonVariant, 3586 source: source, 3587 dispatch: dispatch, 3588 mayHaveSectionsCards: mayHaveSectionsCards 3589 }), newSponsoredLabel && /*#__PURE__*/external_React_default().createElement(DSMessageFooter, { 3590 context_type: context_type, 3591 context: null 3592 })); 3593 }; 3594 class _DSCard extends (external_React_default()).PureComponent { 3595 constructor(props) { 3596 super(props); 3597 this.onLinkClick = this.onLinkClick.bind(this); 3598 this.doesLinkTopicMatchSelectedTopic = this.doesLinkTopicMatchSelectedTopic.bind(this); 3599 this.onMenuUpdate = this.onMenuUpdate.bind(this); 3600 this.onMenuShow = this.onMenuShow.bind(this); 3601 const refinedCardsLayout = this.props.Prefs.values["discoverystream.refinedCardsLayout.enabled"]; 3602 this.setContextMenuButtonHostRef = element => { 3603 this.contextMenuButtonHostElement = element; 3604 }; 3605 this.setPlaceholderRef = element => { 3606 this.placeholderElement = element; 3607 }; 3608 this.state = { 3609 isSeen: false 3610 }; 3611 3612 // If this is for the about:home startup cache, then we always want 3613 // to render the DSCard, regardless of whether or not its been seen. 3614 if (props.App.isForStartupCache.App) { 3615 this.state.isSeen = true; 3616 } 3617 3618 // We want to choose the optimal thumbnail for the underlying DSImage, but 3619 // want to do it in a performant way. The breakpoints used in the 3620 // CSS of the page are, unfortuntely, not easy to retrieve without 3621 // causing a style flush. To avoid that, we hardcode them here. 3622 // 3623 // The values chosen here were the dimensions of the card thumbnails as 3624 // computed by getBoundingClientRect() for each type of viewport width 3625 // across both high-density and normal-density displays. 3626 this.standardCardImageSizes = [{ 3627 mediaMatcher: "default", 3628 width: 296, 3629 height: refinedCardsLayout ? 160 : 148 3630 }]; 3631 this.listCardImageSizes = [{ 3632 mediaMatcher: "(min-width: 1122px)", 3633 width: 75, 3634 height: 75 3635 }, { 3636 mediaMatcher: "default", 3637 width: 50, 3638 height: 50 3639 }]; 3640 this.sectionsCardImagesSizes = { 3641 small: { 3642 width: 110, 3643 height: 117 3644 }, 3645 medium: { 3646 width: 300, 3647 height: refinedCardsLayout ? 160 : 150 3648 }, 3649 large: { 3650 width: 190, 3651 height: 250 3652 } 3653 }; 3654 this.sectionsColumnMediaMatcher = { 3655 1: "default", 3656 2: "(min-width: 724px)", 3657 3: "(min-width: 1122px)", 3658 4: "(min-width: 1390px)" 3659 }; 3660 } 3661 getSectionImageSize(column, size) { 3662 const cardImageSize = { 3663 mediaMatcher: this.sectionsColumnMediaMatcher[column], 3664 width: this.sectionsCardImagesSizes[size].width, 3665 height: this.sectionsCardImagesSizes[size].height 3666 }; 3667 return cardImageSize; 3668 } 3669 doesLinkTopicMatchSelectedTopic() { 3670 // Edge case for clicking on a card when topic selections have not be set 3671 if (!this.props.selectedTopics) { 3672 return "not-set"; 3673 } 3674 3675 // Edge case the topic of the card is not one of the available topics 3676 if (!this.props.availableTopics.includes(this.props.topic)) { 3677 return "topic-not-selectable"; 3678 } 3679 if (this.props.selectedTopics.includes(this.props.topic)) { 3680 return "true"; 3681 } 3682 return "false"; 3683 } 3684 onLinkClick() { 3685 const matchesSelectedTopic = this.doesLinkTopicMatchSelectedTopic(); 3686 if (this.props.dispatch) { 3687 this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({ 3688 event: "CLICK", 3689 source: this.props.type.toUpperCase(), 3690 action_position: this.props.pos, 3691 value: { 3692 event_source: "card", 3693 card_type: this.props.flightId ? "spoc" : "organic", 3694 recommendation_id: this.props.recommendation_id, 3695 tile_id: this.props.id, 3696 ...(this.props.shim && this.props.shim.click ? { 3697 shim: this.props.shim.click 3698 } : {}), 3699 fetchTimestamp: this.props.fetchTimestamp, 3700 firstVisibleTimestamp: this.props.firstVisibleTimestamp, 3701 corpus_item_id: this.props.corpus_item_id, 3702 scheduled_corpus_item_id: this.props.scheduled_corpus_item_id, 3703 recommended_at: this.props.recommended_at, 3704 received_rank: this.props.received_rank, 3705 topic: this.props.topic, 3706 features: this.props.features, 3707 matches_selected_topic: matchesSelectedTopic, 3708 selected_topics: this.props.selectedTopics, 3709 attribution: this.props.attribution, 3710 ...(this.props.format ? { 3711 format: this.props.format 3712 } : { 3713 format: getActiveCardSize(window.innerWidth, this.props.sectionsClassNames, this.props.section, this.props.flightId) 3714 }), 3715 ...(this.props.section ? { 3716 section: this.props.section, 3717 section_position: this.props.sectionPosition, 3718 is_section_followed: this.props.sectionFollowed, 3719 layout_name: this.props.sectionLayoutName 3720 } : {}) 3721 } 3722 })); 3723 this.props.dispatch(actionCreators.ImpressionStats({ 3724 source: this.props.type.toUpperCase(), 3725 click: 0, 3726 window_inner_width: this.props.windowObj.innerWidth, 3727 window_inner_height: this.props.windowObj.innerHeight, 3728 tiles: [{ 3729 id: this.props.id, 3730 pos: this.props.pos, 3731 ...(this.props.shim && this.props.shim.click ? { 3732 shim: this.props.shim.click 3733 } : {}), 3734 type: this.props.flightId ? "spoc" : "organic", 3735 recommendation_id: this.props.recommendation_id, 3736 topic: this.props.topic, 3737 selected_topics: this.props.selectedTopics, 3738 ...(this.props.format ? { 3739 format: this.props.format 3740 } : { 3741 format: getActiveCardSize(window.innerWidth, this.props.sectionsClassNames, this.props.section, this.props.flightId) 3742 }), 3743 ...(this.props.section ? { 3744 section: this.props.section, 3745 section_position: this.props.sectionPosition, 3746 is_section_followed: this.props.sectionFollowed 3747 } : {}) 3748 }] 3749 })); 3750 } 3751 } 3752 onMenuUpdate(showContextMenu) { 3753 if (!showContextMenu) { 3754 const dsLinkMenuHostDiv = this.contextMenuButtonHostElement; 3755 if (dsLinkMenuHostDiv) { 3756 dsLinkMenuHostDiv.classList.remove("active", "last-item"); 3757 } 3758 } 3759 } 3760 async onMenuShow() { 3761 const dsLinkMenuHostDiv = this.contextMenuButtonHostElement; 3762 if (dsLinkMenuHostDiv) { 3763 // Force translation so we can be sure it's ready before measuring. 3764 await this.props.windowObj.document.l10n.translateFragment(dsLinkMenuHostDiv); 3765 if (this.props.windowObj.scrollMaxX > 0) { 3766 dsLinkMenuHostDiv.classList.add("last-item"); 3767 } 3768 dsLinkMenuHostDiv.classList.add("active"); 3769 } 3770 } 3771 onSeen(entries) { 3772 if (this.state) { 3773 const entry = entries.find(e => e.isIntersecting); 3774 if (entry) { 3775 if (this.placeholderElement) { 3776 this.observer.unobserve(this.placeholderElement); 3777 } 3778 3779 // Stop observing since element has been seen 3780 this.setState({ 3781 isSeen: true 3782 }); 3783 } 3784 } 3785 } 3786 onIdleCallback() { 3787 if (!this.state.isSeen) { 3788 // To improve responsiveness without impacting performance, 3789 // we start rendering stories on idle. 3790 // To reduce the number of requests for secure OHTTP images, 3791 // we skip idle-time loading. 3792 if (!this.secureImage) { 3793 if (this.observer && this.placeholderElement) { 3794 this.observer.unobserve(this.placeholderElement); 3795 } 3796 this.setState({ 3797 isSeen: true 3798 }); 3799 } 3800 } 3801 } 3802 componentDidMount() { 3803 this.idleCallbackId = this.props.windowObj.requestIdleCallback(this.onIdleCallback.bind(this)); 3804 if (this.placeholderElement) { 3805 this.observer = new IntersectionObserver(this.onSeen.bind(this)); 3806 this.observer.observe(this.placeholderElement); 3807 } 3808 } 3809 componentWillUnmount() { 3810 // Remove observer on unmount 3811 if (this.observer && this.placeholderElement) { 3812 this.observer.unobserve(this.placeholderElement); 3813 } 3814 if (this.idleCallbackId) { 3815 this.props.windowObj.cancelIdleCallback(this.idleCallbackId); 3816 } 3817 } 3818 3819 // Wraps the image URL with the moz-cached-ohttp:// protocol. 3820 // This enables Firefox to load resources over Oblivious HTTP (OHTTP), 3821 // providing privacy-preserving resource loading. 3822 // Applied only when inferred personalization is enabled. 3823 // See: https://firefox-source-docs.mozilla.org/browser/components/mozcachedohttp/docs/index.html 3824 secureImageURL(url) { 3825 return `moz-cached-ohttp://newtab-image/?url=${encodeURIComponent(url)}`; 3826 } 3827 getRawImageSrc() { 3828 let rawImageSrc = ""; 3829 // There is no point in fetching images for startup cache. 3830 if (!this.props.App.isForStartupCache.App) { 3831 rawImageSrc = this.props.raw_image_src; 3832 } 3833 return rawImageSrc; 3834 } 3835 getFaviconSrc() { 3836 let faviconSrc = ""; 3837 const faviconEnabled = this.props.Prefs.values[PREF_FAVICONS_ENABLED]; 3838 // There is no point in fetching favicons for startup cache. 3839 if (!this.props.App.isForStartupCache.App && faviconEnabled && this.props.icon_src) { 3840 faviconSrc = this.props.icon_src; 3841 if (this.secureImage) { 3842 faviconSrc = this.secureImageURL(this.props.icon_src); 3843 } 3844 } 3845 return faviconSrc; 3846 } 3847 get secureImage() { 3848 const { 3849 Prefs, 3850 flightId 3851 } = this.props; 3852 let ohttpEnabled = false; 3853 if (flightId) { 3854 ohttpEnabled = Prefs.values[PREF_OHTTP_UNIFIED_ADS]; 3855 } else { 3856 ohttpEnabled = Prefs.values[PREF_OHTTP_MERINO]; 3857 } 3858 const ohttpImagesEnabled = Prefs.values.ohttpImagesConfig?.enabled; 3859 const includeTopStoriesSection = Prefs.values.ohttpImagesConfig?.includeTopStoriesSection; 3860 const nonPersonalizedSections = ["top_stories_section"]; 3861 const sectionPersonalized = !nonPersonalizedSections.includes(this.props.section) || includeTopStoriesSection; 3862 const secureImage = ohttpImagesEnabled && ohttpEnabled && sectionPersonalized; 3863 return secureImage; 3864 } 3865 renderImage({ 3866 sizes = [], 3867 classNames = "" 3868 } = {}) { 3869 const { 3870 Prefs 3871 } = this.props; 3872 const rawImageSrc = this.getRawImageSrc(); 3873 const smartCrop = Prefs.values["images.smart"]; 3874 return /*#__PURE__*/external_React_default().createElement(DSImage, { 3875 extraClassNames: `img ${classNames}`, 3876 source: this.props.image_src, 3877 rawSource: rawImageSrc, 3878 sizes: sizes, 3879 url: this.props.url, 3880 title: this.props.title, 3881 isRecentSave: this.props.isRecentSave, 3882 alt_text: this.props.alt_text, 3883 smartCrop: smartCrop, 3884 secureImage: this.secureImage 3885 }); 3886 } 3887 renderSectionCardImages() { 3888 const { 3889 sectionsCardImageSizes 3890 } = this.props; 3891 const columns = ["1", "2", "3", "4"]; 3892 const images = []; 3893 for (const column of columns) { 3894 const size = sectionsCardImageSizes[column]; 3895 const sizes = [this.getSectionImageSize(column, size)]; 3896 const image = this.renderImage({ 3897 sizes, 3898 classNames: `image-${column}` 3899 }); 3900 images.push(image); 3901 } 3902 return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, images); 3903 } 3904 render() { 3905 const { 3906 isRecentSave, 3907 DiscoveryStream, 3908 Prefs, 3909 mayHaveSectionsCards, 3910 format 3911 } = this.props; 3912 const refinedCardsLayout = Prefs.values["discoverystream.refinedCardsLayout.enabled"]; 3913 const refinedCardsClassName = refinedCardsLayout ? `refined-cards` : ``; 3914 if (this.props.placeholder || !this.state.isSeen) { 3915 // placeholder-seen is used to ensure the loading animation is only used if the card is visible. 3916 const placeholderClassName = this.state.isSeen ? `placeholder-seen` : ``; 3917 let placeholderElements = /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("div", { 3918 className: "placeholder-image placeholder-fill" 3919 }), /*#__PURE__*/external_React_default().createElement("div", { 3920 className: "placeholder-label placeholder-fill" 3921 }), /*#__PURE__*/external_React_default().createElement("div", { 3922 className: "placeholder-header placeholder-fill" 3923 }), /*#__PURE__*/external_React_default().createElement("div", { 3924 className: "placeholder-description placeholder-fill" 3925 })); 3926 if (refinedCardsLayout) { 3927 placeholderElements = /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("div", { 3928 className: "placeholder-image placeholder-fill" 3929 }), /*#__PURE__*/external_React_default().createElement("div", { 3930 className: "placeholder-description placeholder-fill" 3931 }), /*#__PURE__*/external_React_default().createElement("div", { 3932 className: "placeholder-header placeholder-fill" 3933 })); 3934 } 3935 return /*#__PURE__*/external_React_default().createElement("div", { 3936 className: `ds-card placeholder ${placeholderClassName} ${refinedCardsClassName}`, 3937 ref: this.setPlaceholderRef 3938 }, placeholderElements); 3939 } 3940 let source = this.props.source || this.props.publisher; 3941 if (!source) { 3942 try { 3943 source = new URL(this.props.url).hostname; 3944 } catch (e) {} 3945 } 3946 const { 3947 hideDescriptions, 3948 compactImages, 3949 imageGradient, 3950 newSponsoredLabel, 3951 titleLines = 3, 3952 descLines = 3, 3953 readTime: displayReadTime 3954 } = DiscoveryStream; 3955 const sectionsEnabled = Prefs.values[DSCard_PREF_SECTIONS_ENABLED]; 3956 // Refined cards have their own excerpt hiding logic. 3957 // We can ignore hideDescriptions if we are in sections and refined cards. 3958 const excerpt = !hideDescriptions || sectionsEnabled && refinedCardsLayout ? this.props.excerpt : ""; 3959 let timeToRead; 3960 if (displayReadTime) { 3961 timeToRead = this.props.time_to_read || readTimeFromWordCount(this.props.word_count); 3962 } 3963 const ctaButtonEnabled = this.props.ctaButtonSponsors?.includes(this.props.sponsor?.toLowerCase()); 3964 let ctaButtonVariant = ""; 3965 if (ctaButtonEnabled) { 3966 ctaButtonVariant = this.props.ctaButtonVariant; 3967 } 3968 let ctaButtonVariantClassName = ctaButtonVariant; 3969 const ctaButtonClassName = ctaButtonEnabled ? `ds-card-cta-button` : ``; 3970 const compactImagesClassName = compactImages ? `ds-card-compact-image` : ``; 3971 const imageGradientClassName = imageGradient ? `ds-card-image-gradient` : ``; 3972 const sectionsCardsClassName = [mayHaveSectionsCards ? `sections-card-ui` : ``, this.props.sectionsClassNames].join(" "); 3973 const titleLinesName = `ds-card-title-lines-${titleLines}`; 3974 const descLinesClassName = `ds-card-desc-lines-${descLines}`; 3975 const isMediumRectangle = format === "rectangle"; 3976 const spocFormatClassName = isMediumRectangle ? `ds-spoc-rectangle` : ``; 3977 const faviconSrc = this.getFaviconSrc(); 3978 let images = this.renderImage({ 3979 sizes: this.standardCardImageSizes 3980 }); 3981 if (isMediumRectangle) { 3982 images = this.renderImage(); 3983 } else if (sectionsEnabled) { 3984 images = this.renderSectionCardImages(); 3985 } 3986 return /*#__PURE__*/external_React_default().createElement("article", { 3987 className: `ds-card ${sectionsCardsClassName} ${compactImagesClassName} ${imageGradientClassName} ${titleLinesName} ${descLinesClassName} ${spocFormatClassName} ${ctaButtonClassName} ${ctaButtonVariantClassName} ${refinedCardsClassName}`, 3988 ref: this.setContextMenuButtonHostRef, 3989 "data-position-one": this.props["data-position-one"], 3990 "data-position-two": this.props["data-position-one"], 3991 "data-position-three": this.props["data-position-one"], 3992 "data-position-four": this.props["data-position-one"] 3993 }, /*#__PURE__*/external_React_default().createElement(SafeAnchor, { 3994 className: "ds-card-link", 3995 dispatch: this.props.dispatch, 3996 onLinkClick: !this.props.placeholder ? this.onLinkClick : undefined, 3997 url: this.props.url, 3998 title: this.props.title, 3999 isSponsored: !!this.props.flightId, 4000 tabIndex: this.props.tabIndex, 4001 onFocus: this.props.onFocus 4002 }, this.props.showTopics && !this.props.mayHaveSectionsCards && this.props.topic && !refinedCardsLayout && /*#__PURE__*/external_React_default().createElement("span", { 4003 className: "ds-card-topic", 4004 "data-l10n-id": `newtab-topic-label-${this.props.topic}` 4005 }), /*#__PURE__*/external_React_default().createElement("div", { 4006 className: "img-wrapper" 4007 }, images), /*#__PURE__*/external_React_default().createElement(ImpressionStats_ImpressionStats, { 4008 flightId: this.props.flightId, 4009 rows: [{ 4010 id: this.props.id, 4011 pos: this.props.pos, 4012 ...(this.props.shim && this.props.shim.impression ? { 4013 shim: this.props.shim.impression 4014 } : {}), 4015 recommendation_id: this.props.recommendation_id, 4016 fetchTimestamp: this.props.fetchTimestamp, 4017 corpus_item_id: this.props.corpus_item_id, 4018 scheduled_corpus_item_id: this.props.scheduled_corpus_item_id, 4019 recommended_at: this.props.recommended_at, 4020 received_rank: this.props.received_rank, 4021 topic: this.props.topic, 4022 features: this.props.features, 4023 ...(format ? { 4024 format 4025 } : {}), 4026 category: this.props.category, 4027 attribution: this.props.attribution, 4028 ...(this.props.section ? { 4029 section: this.props.section, 4030 section_position: this.props.sectionPosition, 4031 is_section_followed: this.props.sectionFollowed, 4032 sectionLayoutName: this.props.sectionLayoutName 4033 } : {}), 4034 ...(!format && this.props.section ? 4035 // Note: sectionsCardsClassName is passed to ImpressionStats.jsx in order to calculate format 4036 { 4037 class_names: sectionsCardsClassName 4038 } : {}) 4039 }], 4040 dispatch: this.props.dispatch, 4041 source: this.props.type, 4042 firstVisibleTimestamp: this.props.firstVisibleTimestamp 4043 }), ctaButtonVariant === "variant-b" && /*#__PURE__*/external_React_default().createElement("div", { 4044 className: "cta-header" 4045 }, "Shop Now"), /*#__PURE__*/external_React_default().createElement(DefaultMeta, { 4046 source: source, 4047 title: this.props.title, 4048 excerpt: excerpt, 4049 newSponsoredLabel: newSponsoredLabel, 4050 timeToRead: timeToRead, 4051 context: this.props.context, 4052 context_type: this.props.context_type, 4053 sponsor: this.props.sponsor, 4054 sponsored_by_override: this.props.sponsored_by_override, 4055 ctaButtonVariant: ctaButtonVariant, 4056 dispatch: this.props.dispatch, 4057 mayHaveSectionsCards: this.props.mayHaveSectionsCards, 4058 state: this.state, 4059 showTopics: !refinedCardsLayout && this.props.showTopics, 4060 isSectionsCard: this.props.mayHaveSectionsCards && this.props.topic, 4061 format: format, 4062 topic: this.props.topic, 4063 icon_src: faviconSrc, 4064 refinedCardsLayout: refinedCardsLayout, 4065 tabIndex: this.props.tabIndex 4066 })), /*#__PURE__*/external_React_default().createElement("div", { 4067 className: "card-stp-button-hover-background" 4068 }, /*#__PURE__*/external_React_default().createElement("div", { 4069 className: "card-stp-button-position-wrapper" 4070 }, /*#__PURE__*/external_React_default().createElement(DSLinkMenu, { 4071 id: this.props.id, 4072 index: this.props.pos, 4073 dispatch: this.props.dispatch, 4074 url: this.props.url, 4075 title: this.props.title, 4076 source: source, 4077 type: this.props.type, 4078 card_type: this.props.flightId ? "spoc" : "organic", 4079 pocket_id: this.props.pocket_id, 4080 shim: this.props.shim, 4081 bookmarkGuid: this.props.bookmarkGuid, 4082 flightId: this.props.flightId, 4083 showPrivacyInfo: !!this.props.flightId, 4084 onMenuUpdate: this.onMenuUpdate, 4085 onMenuShow: this.onMenuShow, 4086 isRecentSave: isRecentSave, 4087 recommendation_id: this.props.recommendation_id, 4088 tile_id: this.props.id, 4089 block_key: this.props.id, 4090 corpus_item_id: this.props.corpus_item_id, 4091 scheduled_corpus_item_id: this.props.scheduled_corpus_item_id, 4092 recommended_at: this.props.recommended_at, 4093 received_rank: this.props.received_rank, 4094 section: this.props.section, 4095 section_position: this.props.sectionPosition, 4096 is_section_followed: this.props.sectionFollowed, 4097 fetchTimestamp: this.props.fetchTimestamp, 4098 firstVisibleTimestamp: this.props.firstVisibleTimestamp, 4099 format: format ? format : getActiveCardSize(window.innerWidth, this.props.sectionsClassNames, this.props.section, this.props.flightId), 4100 isSectionsCard: this.props.mayHaveSectionsCards, 4101 topic: this.props.topic, 4102 selected_topics: this.props.selected_topics, 4103 tabIndex: this.props.tabIndex 4104 })))); 4105 } 4106 } 4107 _DSCard.defaultProps = { 4108 windowObj: window // Added to support unit tests 4109 }; 4110 const DSCard = (0,external_ReactRedux_namespaceObject.connect)(state => ({ 4111 App: state.App, 4112 DiscoveryStream: state.DiscoveryStream, 4113 Prefs: state.Prefs 4114 }))(_DSCard); 4115 const PlaceholderDSCard = () => /*#__PURE__*/external_React_default().createElement(DSCard, { 4116 placeholder: true 4117 }); 4118 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx 4119 /* This Source Code Form is subject to the terms of the Mozilla Public 4120 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 4121 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4122 4123 4124 4125 class DSEmptyState extends (external_React_default()).PureComponent { 4126 constructor(props) { 4127 super(props); 4128 this.onReset = this.onReset.bind(this); 4129 this.state = {}; 4130 } 4131 componentWillUnmount() { 4132 if (this.timeout) { 4133 clearTimeout(this.timeout); 4134 } 4135 } 4136 onReset() { 4137 if (this.props.dispatch && this.props.feed) { 4138 const { 4139 feed 4140 } = this.props; 4141 const { 4142 url 4143 } = feed; 4144 this.props.dispatch({ 4145 type: actionTypes.DISCOVERY_STREAM_FEED_UPDATE, 4146 data: { 4147 feed: { 4148 ...feed, 4149 data: { 4150 ...feed.data, 4151 status: "waiting" 4152 } 4153 }, 4154 url 4155 } 4156 }); 4157 this.setState({ 4158 waiting: true 4159 }); 4160 this.timeout = setTimeout(() => { 4161 this.timeout = null; 4162 this.setState({ 4163 waiting: false 4164 }); 4165 }, 300); 4166 this.props.dispatch(actionCreators.OnlyToMain({ 4167 type: actionTypes.DISCOVERY_STREAM_RETRY_FEED, 4168 data: { 4169 feed 4170 } 4171 })); 4172 } 4173 } 4174 renderButton() { 4175 if (this.props.status === "waiting" || this.state.waiting) { 4176 return /*#__PURE__*/external_React_default().createElement("button", { 4177 className: "try-again-button waiting", 4178 "data-l10n-id": "newtab-discovery-empty-section-topstories-loading" 4179 }); 4180 } 4181 return /*#__PURE__*/external_React_default().createElement("button", { 4182 className: "try-again-button", 4183 onClick: this.onReset, 4184 "data-l10n-id": "newtab-discovery-empty-section-topstories-try-again-button" 4185 }); 4186 } 4187 renderState() { 4188 if (this.props.status === "waiting" || this.props.status === "failed") { 4189 return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("h2", { 4190 "data-l10n-id": "newtab-discovery-empty-section-topstories-timed-out" 4191 }), this.renderButton()); 4192 } 4193 return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("h2", { 4194 "data-l10n-id": "newtab-discovery-empty-section-topstories-header" 4195 }), /*#__PURE__*/external_React_default().createElement("p", { 4196 "data-l10n-id": "newtab-discovery-empty-section-topstories-content" 4197 })); 4198 } 4199 render() { 4200 return /*#__PURE__*/external_React_default().createElement("div", { 4201 className: "section-empty-state" 4202 }, /*#__PURE__*/external_React_default().createElement("div", { 4203 className: "empty-state-message" 4204 }, this.renderState())); 4205 } 4206 } 4207 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx 4208 /* This Source Code Form is subject to the terms of the Mozilla Public 4209 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 4210 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4211 4212 4213 4214 4215 4216 4217 function _TopicsWidget(props) { 4218 const { 4219 id, 4220 source, 4221 position, 4222 DiscoveryStream, 4223 dispatch 4224 } = props; 4225 const { 4226 utmCampaign, 4227 utmContent, 4228 utmSource 4229 } = DiscoveryStream.experimentData; 4230 let queryParams = `?utm_source=${utmSource}`; 4231 if (utmCampaign && utmContent) { 4232 queryParams += `&utm_content=${utmContent}&utm_campaign=${utmCampaign}`; 4233 } 4234 const topics = [{ 4235 label: "Technology", 4236 name: "technology" 4237 }, { 4238 label: "Science", 4239 name: "science" 4240 }, { 4241 label: "Self-Improvement", 4242 name: "self-improvement" 4243 }, { 4244 label: "Travel", 4245 name: "travel" 4246 }, { 4247 label: "Career", 4248 name: "career" 4249 }, { 4250 label: "Entertainment", 4251 name: "entertainment" 4252 }, { 4253 label: "Food", 4254 name: "food" 4255 }, { 4256 label: "Health", 4257 name: "health" 4258 }, { 4259 label: "Must-Reads", 4260 name: "must-reads", 4261 url: `https://getpocket.com/collections${queryParams}` 4262 }]; 4263 function onLinkClick(topic, positionInCard) { 4264 if (dispatch) { 4265 dispatch(actionCreators.DiscoveryStreamUserEvent({ 4266 event: "CLICK", 4267 source, 4268 action_position: position, 4269 value: { 4270 card_type: "topics_widget", 4271 topic, 4272 ...(positionInCard || positionInCard === 0 ? { 4273 position_in_card: positionInCard 4274 } : {}), 4275 section_position: position 4276 } 4277 })); 4278 dispatch(actionCreators.ImpressionStats({ 4279 source, 4280 click: 0, 4281 window_inner_width: props.windowObj.innerWidth, 4282 window_inner_height: props.windowObj.innerHeight, 4283 tiles: [{ 4284 id, 4285 pos: position 4286 }] 4287 })); 4288 } 4289 } 4290 function mapTopicItem(topic, index) { 4291 return /*#__PURE__*/external_React_default().createElement("li", { 4292 key: topic.name, 4293 className: topic.overflow ? "ds-topics-widget-list-overflow-item" : "" 4294 }, /*#__PURE__*/external_React_default().createElement(SafeAnchor, { 4295 url: topic.url || `https://getpocket.com/explore/${topic.name}${queryParams}`, 4296 dispatch: dispatch, 4297 onLinkClick: () => onLinkClick(topic.name, index) 4298 }, topic.label)); 4299 } 4300 return /*#__PURE__*/external_React_default().createElement("div", { 4301 className: "ds-topics-widget" 4302 }, /*#__PURE__*/external_React_default().createElement("header", { 4303 className: "ds-topics-widget-header" 4304 }, "Popular Topics"), /*#__PURE__*/external_React_default().createElement("hr", null), /*#__PURE__*/external_React_default().createElement("div", { 4305 className: "ds-topics-widget-list-container" 4306 }, /*#__PURE__*/external_React_default().createElement("ul", null, topics.map(mapTopicItem))), /*#__PURE__*/external_React_default().createElement(SafeAnchor, { 4307 className: "ds-topics-widget-button button primary", 4308 url: `https://getpocket.com/${queryParams}`, 4309 dispatch: dispatch, 4310 onLinkClick: () => onLinkClick("more-topics") 4311 }, "More Topics"), /*#__PURE__*/external_React_default().createElement(ImpressionStats_ImpressionStats, { 4312 dispatch: dispatch, 4313 rows: [{ 4314 id, 4315 pos: position 4316 }], 4317 source: source 4318 })); 4319 } 4320 _TopicsWidget.defaultProps = { 4321 windowObj: window // Added to support unit tests 4322 }; 4323 const TopicsWidget = (0,external_ReactRedux_namespaceObject.connect)(state => ({ 4324 DiscoveryStream: state.DiscoveryStream 4325 }))(_TopicsWidget); 4326 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/AdBannerContextMenu/AdBannerContextMenu.jsx 4327 /* This Source Code Form is subject to the terms of the Mozilla Public 4328 * License, v. 2.0. If a copy of the MPL was not distributed with this 4329 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4330 4331 4332 4333 4334 4335 /** 4336 * A context menu for IAB banners (e.g. billboard, leaderboard). 4337 * 4338 * Note: MREC ad formats and sponsored stories share the context menu with 4339 * other cards: make sure you also look at DSLinkMenu component 4340 * to keep any updates to ad-related context menu items in sync. 4341 * 4342 * @param dispatch 4343 * @param spoc 4344 * @param position 4345 * @param type 4346 * @param showAdReporting 4347 * @returns {Element} 4348 * @class 4349 */ 4350 function AdBannerContextMenu({ 4351 dispatch, 4352 spoc, 4353 position, 4354 type, 4355 showAdReporting, 4356 toggleActive = () => {} 4357 }) { 4358 const ADBANNER_CONTEXT_MENU_OPTIONS = ["BlockAdUrl", ...(showAdReporting ? ["ReportAd"] : []), "ManageSponsoredContent", "OurSponsorsAndYourPrivacy"]; 4359 const [showContextMenu, setShowContextMenu] = (0,external_React_namespaceObject.useState)(false); 4360 const [contextMenuClassNames, setContextMenuClassNames] = (0,external_React_namespaceObject.useState)("ads-context-menu"); 4361 4362 // The keyboard access parameter is passed down to LinkMenu component 4363 // that uses it to focus on the first context menu option for accessibility. 4364 const [isKeyboardAccess, setIsKeyboardAccess] = (0,external_React_namespaceObject.useState)(false); 4365 4366 /** 4367 * Toggles the style fix for context menu hover/active styles. 4368 * This allows us to have unobtrusive, transparent button background by default, 4369 * yet flip it over to semi-transparent grey when the menu is visible. 4370 * 4371 * @param contextMenuOpen 4372 */ 4373 const toggleContextMenuStyleSwitch = contextMenuOpen => { 4374 if (contextMenuOpen) { 4375 setContextMenuClassNames("ads-context-menu context-menu-open"); 4376 } else { 4377 setContextMenuClassNames("ads-context-menu"); 4378 } 4379 }; 4380 4381 /** 4382 * Toggles the context menu to open or close. Sets state depending on whether 4383 * the context menu is accessed by mouse or keyboard. 4384 * 4385 * @param isKeyBoard 4386 */ 4387 const toggleContextMenu = isKeyBoard => { 4388 toggleContextMenuStyleSwitch(!showContextMenu); 4389 toggleActive(!showContextMenu); 4390 setShowContextMenu(!showContextMenu); 4391 setIsKeyboardAccess(isKeyBoard); 4392 }; 4393 const onClick = e => { 4394 e.preventDefault(); 4395 toggleContextMenu(false); 4396 }; 4397 const onKeyDown = e => { 4398 if (e.key === "Enter" || e.key === " ") { 4399 e.preventDefault(); 4400 toggleContextMenu(true); 4401 } 4402 }; 4403 const onUpdate = () => { 4404 toggleContextMenuStyleSwitch(!showContextMenu); 4405 toggleActive(!showContextMenu); 4406 setShowContextMenu(!showContextMenu); 4407 }; 4408 return /*#__PURE__*/external_React_default().createElement("div", { 4409 className: "ads-context-menu-wrapper" 4410 }, /*#__PURE__*/external_React_default().createElement("div", { 4411 className: contextMenuClassNames 4412 }, /*#__PURE__*/external_React_default().createElement("moz-button", { 4413 type: "icon", 4414 size: "default", 4415 "data-l10n-id": "newtab-menu-content-tooltip", 4416 "data-l10n-args": JSON.stringify({ 4417 title: spoc.title || spoc.sponsor || spoc.alt_text 4418 }), 4419 iconsrc: "chrome://global/skin/icons/more.svg", 4420 onClick: onClick, 4421 onKeyDown: onKeyDown 4422 }), showContextMenu && /*#__PURE__*/external_React_default().createElement(LinkMenu, { 4423 onUpdate: onUpdate, 4424 dispatch: dispatch, 4425 keyboardAccess: isKeyboardAccess, 4426 options: ADBANNER_CONTEXT_MENU_OPTIONS, 4427 shouldSendImpressionStats: true, 4428 userEvent: actionCreators.DiscoveryStreamUserEvent, 4429 site: { 4430 // Props we want to pass on for new ad types that come from Unified Ads API 4431 block_key: spoc.block_key, 4432 fetchTimestamp: spoc.fetchTimestamp, 4433 flight_id: spoc.flight_id, 4434 format: spoc.format, 4435 id: spoc.id, 4436 guid: spoc.guid, 4437 card_type: "spoc", 4438 // required to record telemetry for an action, see handleBlockUrl in TelemetryFeed.sys.mjs 4439 is_pocket_card: true, 4440 position, 4441 sponsor: spoc.sponsor, 4442 title: spoc.title, 4443 url: spoc.url || spoc.shim.url, 4444 personalization_models: spoc.personalization_models, 4445 priority: spoc.priority, 4446 score: spoc.score, 4447 alt_text: spoc.alt_text, 4448 shim: spoc.shim 4449 }, 4450 index: position, 4451 source: type.toUpperCase() 4452 }))); 4453 } 4454 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/PromoCard/PromoCard.jsx 4455 /* This Source Code Form is subject to the terms of the Mozilla Public 4456 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 4457 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4458 4459 4460 4461 4462 4463 const PREF_PROMO_CARD_DISMISSED = "discoverystream.promoCard.visible"; 4464 4465 /** 4466 * The PromoCard component displays a promotional message. 4467 * It is used next to the AdBanner component in a four-column layout. 4468 */ 4469 4470 const PromoCard = () => { 4471 const dispatch = (0,external_ReactRedux_namespaceObject.useDispatch)(); 4472 const onCtaClick = (0,external_React_namespaceObject.useCallback)(() => { 4473 dispatch(actionCreators.AlsoToMain({ 4474 type: actionTypes.PROMO_CARD_CLICK 4475 })); 4476 }, [dispatch]); 4477 const onDismissClick = (0,external_React_namespaceObject.useCallback)(() => { 4478 dispatch(actionCreators.AlsoToMain({ 4479 type: actionTypes.PROMO_CARD_DISMISS 4480 })); 4481 dispatch(actionCreators.SetPref(PREF_PROMO_CARD_DISMISSED, false)); 4482 }, [dispatch]); 4483 const handleIntersection = (0,external_React_namespaceObject.useCallback)(() => { 4484 dispatch(actionCreators.AlsoToMain({ 4485 type: actionTypes.PROMO_CARD_IMPRESSION 4486 })); 4487 }, [dispatch]); 4488 const ref = useIntersectionObserver(handleIntersection); 4489 return /*#__PURE__*/external_React_default().createElement("div", { 4490 className: "promo-card-wrapper", 4491 ref: el => { 4492 ref.current = [el]; 4493 } 4494 }, /*#__PURE__*/external_React_default().createElement("div", { 4495 className: "promo-card-dismiss-button" 4496 }, /*#__PURE__*/external_React_default().createElement("moz-button", { 4497 type: "icon ghost", 4498 size: "small", 4499 "data-l10n-id": "newtab-promo-card-dismiss-button", 4500 iconsrc: "chrome://global/skin/icons/close.svg", 4501 onClick: onDismissClick, 4502 onKeyDown: onDismissClick 4503 })), /*#__PURE__*/external_React_default().createElement("div", { 4504 className: "promo-card-inner" 4505 }, /*#__PURE__*/external_React_default().createElement("div", { 4506 className: "img-wrapper" 4507 }, /*#__PURE__*/external_React_default().createElement("img", { 4508 src: "chrome://newtab/content/data/content/assets/puzzle-fox.svg", 4509 alt: "" 4510 })), /*#__PURE__*/external_React_default().createElement("span", { 4511 className: "promo-card-title", 4512 "data-l10n-id": "newtab-promo-card-title" 4513 }), /*#__PURE__*/external_React_default().createElement("span", { 4514 className: "promo-card-body", 4515 "data-l10n-id": "newtab-promo-card-body" 4516 }), /*#__PURE__*/external_React_default().createElement("span", { 4517 className: "promo-card-cta-wrapper" 4518 }, /*#__PURE__*/external_React_default().createElement("a", { 4519 href: "https://support.mozilla.org/kb/sponsor-privacy", 4520 "data-l10n-id": "newtab-promo-card-cta", 4521 target: "_blank", 4522 rel: "noreferrer", 4523 onClick: onCtaClick 4524 })))); 4525 }; 4526 4527 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/AdBanner/AdBanner.jsx 4528 /* This Source Code Form is subject to the terms of the Mozilla Public 4529 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 4530 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4531 4532 4533 4534 4535 4536 4537 4538 const AdBanner_PREF_SECTIONS_ENABLED = "discoverystream.sections.enabled"; 4539 const AdBanner_PREF_OHTTP_UNIFIED_ADS = "unifiedAds.ohttp.enabled"; 4540 const PREF_REPORT_ADS_ENABLED = "discoverystream.reportAds.enabled"; 4541 const PREF_PROMOCARD_ENABLED = "discoverystream.promoCard.enabled"; 4542 const PREF_PROMOCARD_VISIBLE = "discoverystream.promoCard.visible"; 4543 4544 /** 4545 * A new banner ad that appears between rows of stories: leaderboard or billboard size. 4546 * 4547 * @param spoc 4548 * @param dispatch 4549 * @param firstVisibleTimestamp 4550 * @param row 4551 * @param type 4552 * @param prefs 4553 * @returns {Element} 4554 * @class 4555 */ 4556 const AdBanner = ({ 4557 spoc, 4558 dispatch, 4559 firstVisibleTimestamp, 4560 row, 4561 type, 4562 prefs 4563 }) => { 4564 const getDimensions = format => { 4565 switch (format) { 4566 case "leaderboard": 4567 return { 4568 width: "728", 4569 height: "90" 4570 }; 4571 case "billboard": 4572 return { 4573 width: "970", 4574 height: "250" 4575 }; 4576 } 4577 return { 4578 // image will still render with default values 4579 width: undefined, 4580 height: undefined 4581 }; 4582 }; 4583 const promoCardEnabled = spoc.format === "billboard" && prefs[PREF_PROMOCARD_ENABLED] && prefs[PREF_PROMOCARD_VISIBLE]; 4584 const sectionsEnabled = prefs[AdBanner_PREF_SECTIONS_ENABLED]; 4585 const ohttpEnabled = prefs[AdBanner_PREF_OHTTP_UNIFIED_ADS]; 4586 const showAdReporting = prefs[PREF_REPORT_ADS_ENABLED]; 4587 const ohttpImagesEnabled = prefs.ohttpImagesConfig?.enabled; 4588 const [menuActive, setMenuActive] = (0,external_React_namespaceObject.useState)(false); 4589 const adBannerWrapperClassName = `ad-banner-wrapper ${menuActive ? "active" : ""} ${promoCardEnabled ? "promo-card" : ""}`; 4590 const { 4591 width: imgWidth, 4592 height: imgHeight 4593 } = getDimensions(spoc.format); 4594 const onLinkClick = () => { 4595 dispatch(actionCreators.DiscoveryStreamUserEvent({ 4596 event: "CLICK", 4597 source: type.toUpperCase(), 4598 // Banner ads don't have a position, but a row number 4599 action_position: parseInt(row, 10), 4600 value: { 4601 card_type: "spoc", 4602 tile_id: spoc.id, 4603 ...(spoc.shim?.click ? { 4604 shim: spoc.shim.click 4605 } : {}), 4606 fetchTimestamp: spoc.fetchTimestamp, 4607 firstVisibleTimestamp, 4608 format: spoc.format, 4609 ...(sectionsEnabled ? { 4610 section: spoc.format, 4611 section_position: parseInt(row, 10) 4612 } : {}) 4613 } 4614 })); 4615 }; 4616 const toggleActive = active => { 4617 setMenuActive(active); 4618 }; 4619 4620 // in the default card grid 1 would come before the 1st row of cards and 9 comes after the last row 4621 // using clamp to make sure its between valid values (1-9) 4622 const clampedRow = Math.max(1, Math.min(9, row)); 4623 const secureImage = ohttpImagesEnabled && ohttpEnabled; 4624 let rawImageSrc = spoc.raw_image_src; 4625 4626 // Wraps the image URL with the moz-cached-ohttp:// protocol. 4627 // This enables Firefox to load resources over Oblivious HTTP (OHTTP), 4628 // providing privacy-preserving resource loading. 4629 // Applied only when inferred personalization is enabled. 4630 // See: https://firefox-source-docs.mozilla.org/browser/components/mozcachedohttp/docs/index.html 4631 if (secureImage) { 4632 rawImageSrc = `moz-cached-ohttp://newtab-image/?url=${encodeURIComponent(spoc.raw_image_src)}`; 4633 } 4634 return /*#__PURE__*/external_React_default().createElement("aside", { 4635 className: adBannerWrapperClassName, 4636 style: { 4637 gridRow: clampedRow 4638 } 4639 }, /*#__PURE__*/external_React_default().createElement("div", { 4640 className: `ad-banner-inner ${spoc.format}` 4641 }, /*#__PURE__*/external_React_default().createElement(SafeAnchor, { 4642 className: "ad-banner-link", 4643 url: spoc.url, 4644 title: spoc.title || spoc.sponsor || spoc.alt_text, 4645 onLinkClick: onLinkClick, 4646 dispatch: dispatch, 4647 isSponsored: true 4648 }, /*#__PURE__*/external_React_default().createElement(ImpressionStats_ImpressionStats, { 4649 flightId: spoc.flight_id, 4650 rows: [{ 4651 id: spoc.id, 4652 card_type: "spoc", 4653 pos: row, 4654 recommended_at: spoc.recommended_at, 4655 received_rank: spoc.received_rank, 4656 format: spoc.format, 4657 ...(spoc.shim?.impression ? { 4658 shim: spoc.shim.impression 4659 } : {}) 4660 }], 4661 dispatch: dispatch, 4662 firstVisibleTimestamp: firstVisibleTimestamp 4663 }), /*#__PURE__*/external_React_default().createElement("div", { 4664 className: "ad-banner-content" 4665 }, /*#__PURE__*/external_React_default().createElement("img", { 4666 src: rawImageSrc, 4667 alt: spoc.alt_text, 4668 loading: "eager", 4669 width: imgWidth, 4670 height: imgHeight 4671 })), /*#__PURE__*/external_React_default().createElement("div", { 4672 className: "ad-banner-sponsored" 4673 }, /*#__PURE__*/external_React_default().createElement("span", { 4674 className: "ad-banner-sponsored-label", 4675 "data-l10n-id": "newtab-label-sponsored-fixed" 4676 }))), /*#__PURE__*/external_React_default().createElement("div", { 4677 className: "ad-banner-hover-background" 4678 }, /*#__PURE__*/external_React_default().createElement(AdBannerContextMenu, { 4679 dispatch: dispatch, 4680 spoc: spoc, 4681 position: row, 4682 type: type, 4683 showAdReporting: showAdReporting, 4684 toggleActive: toggleActive 4685 }))), promoCardEnabled && /*#__PURE__*/external_React_default().createElement(PromoCard, null)); 4686 }; 4687 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx 4688 /* This Source Code Form is subject to the terms of the Mozilla Public 4689 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 4690 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4691 4692 4693 4694 4695 4696 4697 4698 4699 const PREF_SECTIONS_CARDS_ENABLED = "discoverystream.sections.cards.enabled"; 4700 const PREF_TOPICS_ENABLED = "discoverystream.topicLabels.enabled"; 4701 const PREF_TOPICS_SELECTED = "discoverystream.topicSelection.selectedTopics"; 4702 const PREF_TOPICS_AVAILABLE = "discoverystream.topicSelection.topics"; 4703 const PREF_SPOCS_STARTUPCACHE_ENABLED = "discoverystream.spocs.startupCache.enabled"; 4704 const PREF_BILLBOARD_ENABLED = "newtabAdSize.billboard"; 4705 const PREF_BILLBOARD_POSITION = "newtabAdSize.billboard.position"; 4706 const PREF_LEADERBOARD_ENABLED = "newtabAdSize.leaderboard"; 4707 const PREF_LEADERBOARD_POSITION = "newtabAdSize.leaderboard.position"; 4708 const WIDGET_IDS = { 4709 TOPICS: 1 4710 }; 4711 function DSSubHeader({ 4712 children 4713 }) { 4714 return /*#__PURE__*/external_React_default().createElement("div", { 4715 className: "section-top-bar ds-sub-header" 4716 }, /*#__PURE__*/external_React_default().createElement("h3", { 4717 className: "section-title-container" 4718 }, children)); 4719 } 4720 4721 // eslint-disable-next-line no-shadow 4722 function CardGrid_IntersectionObserver({ 4723 children, 4724 windowObj = window, 4725 onIntersecting 4726 }) { 4727 const intersectionElement = (0,external_React_namespaceObject.useRef)(null); 4728 (0,external_React_namespaceObject.useEffect)(() => { 4729 let observer; 4730 if (!observer && onIntersecting && intersectionElement.current) { 4731 observer = new windowObj.IntersectionObserver(entries => { 4732 const entry = entries.find(e => e.isIntersecting); 4733 if (entry) { 4734 // Stop observing since element has been seen 4735 if (observer && intersectionElement.current) { 4736 observer.unobserve(intersectionElement.current); 4737 } 4738 onIntersecting(); 4739 } 4740 }); 4741 observer.observe(intersectionElement.current); 4742 } 4743 // Cleanup 4744 return () => observer?.disconnect(); 4745 }, [windowObj, onIntersecting]); 4746 return /*#__PURE__*/external_React_default().createElement("div", { 4747 ref: intersectionElement 4748 }, children); 4749 } 4750 class _CardGrid extends (external_React_default()).PureComponent { 4751 constructor(props) { 4752 super(props); 4753 this.state = { 4754 focusedIndex: 0 4755 }; 4756 this.onCardFocus = this.onCardFocus.bind(this); 4757 this.handleCardKeyDown = this.handleCardKeyDown.bind(this); 4758 } 4759 onCardFocus(index) { 4760 this.setState({ 4761 focusedIndex: index 4762 }); 4763 } 4764 handleCardKeyDown(e) { 4765 if (e.key === "ArrowLeft" || e.key === "ArrowRight") { 4766 e.preventDefault(); 4767 const currentCardEl = e.target.closest("article.ds-card"); 4768 if (!currentCardEl) { 4769 return; 4770 } 4771 4772 // Arrow direction should match visual navigation direction in RTL 4773 const isRTL = document.dir === "rtl"; 4774 const navigateToPrevious = isRTL ? e.key === "ArrowRight" : e.key === "ArrowLeft"; 4775 let targetCardEl = currentCardEl; 4776 4777 // Walk through siblings to find the target card element 4778 while (targetCardEl) { 4779 targetCardEl = navigateToPrevious ? targetCardEl.previousElementSibling : targetCardEl.nextElementSibling; 4780 if (targetCardEl && targetCardEl.matches("article.ds-card")) { 4781 const link = targetCardEl.querySelector("a.ds-card-link"); 4782 if (link) { 4783 link.focus(); 4784 } 4785 break; 4786 } 4787 } 4788 } 4789 } 4790 4791 // eslint-disable-next-line max-statements 4792 renderCards() { 4793 const prefs = this.props.Prefs.values; 4794 const { 4795 items, 4796 ctaButtonSponsors, 4797 ctaButtonVariant, 4798 widgets, 4799 DiscoveryStream 4800 } = this.props; 4801 const { 4802 topicsLoading 4803 } = DiscoveryStream; 4804 const mayHaveSectionsCards = prefs[PREF_SECTIONS_CARDS_ENABLED]; 4805 const showTopics = prefs[PREF_TOPICS_ENABLED]; 4806 const selectedTopics = prefs[PREF_TOPICS_SELECTED]; 4807 const availableTopics = prefs[PREF_TOPICS_AVAILABLE]; 4808 const spocsStartupCacheEnabled = prefs[PREF_SPOCS_STARTUPCACHE_ENABLED]; 4809 const billboardEnabled = prefs[PREF_BILLBOARD_ENABLED]; 4810 const leaderboardEnabled = prefs[PREF_LEADERBOARD_ENABLED]; 4811 const recs = this.props.data.recommendations.slice(0, items); 4812 const cards = []; 4813 let cardIndex = 0; 4814 for (let index = 0; index < items; index++) { 4815 const rec = recs[index]; 4816 const isPlaceholder = topicsLoading || this.props.placeholder || !rec || rec.placeholder || rec.flight_id && !spocsStartupCacheEnabled && this.props.App.isForStartupCache.DiscoveryStream; 4817 if (isPlaceholder) { 4818 cards.push(/*#__PURE__*/external_React_default().createElement(PlaceholderDSCard, { 4819 key: `dscard-${index}` 4820 })); 4821 } else { 4822 const currentCardIndex = cardIndex; 4823 cardIndex++; 4824 cards.push(/*#__PURE__*/external_React_default().createElement(DSCard, { 4825 key: `dscard-${rec.id}`, 4826 pos: rec.pos, 4827 flightId: rec.flight_id, 4828 image_src: rec.image_src, 4829 raw_image_src: rec.raw_image_src, 4830 icon_src: rec.icon_src, 4831 word_count: rec.word_count, 4832 time_to_read: rec.time_to_read, 4833 title: rec.title, 4834 topic: rec.topic, 4835 features: rec.features, 4836 showTopics: showTopics, 4837 selectedTopics: selectedTopics, 4838 excerpt: rec.excerpt, 4839 availableTopics: availableTopics, 4840 url: rec.url, 4841 id: rec.id, 4842 shim: rec.shim, 4843 fetchTimestamp: rec.fetchTimestamp, 4844 type: this.props.type, 4845 context: rec.context, 4846 sponsor: rec.sponsor, 4847 sponsored_by_override: rec.sponsored_by_override, 4848 dispatch: this.props.dispatch, 4849 source: rec.domain, 4850 publisher: rec.publisher, 4851 pocket_id: rec.pocket_id, 4852 context_type: rec.context_type, 4853 bookmarkGuid: rec.bookmarkGuid, 4854 ctaButtonSponsors: ctaButtonSponsors, 4855 ctaButtonVariant: ctaButtonVariant, 4856 recommendation_id: rec.recommendation_id, 4857 firstVisibleTimestamp: this.props.firstVisibleTimestamp, 4858 mayHaveSectionsCards: mayHaveSectionsCards, 4859 corpus_item_id: rec.corpus_item_id, 4860 scheduled_corpus_item_id: rec.scheduled_corpus_item_id, 4861 recommended_at: rec.recommended_at, 4862 received_rank: rec.received_rank, 4863 format: rec.format, 4864 alt_text: rec.alt_text, 4865 isTimeSensitive: rec.isTimeSensitive, 4866 tabIndex: currentCardIndex === this.state.focusedIndex ? 0 : -1, 4867 onFocus: () => this.onCardFocus(currentCardIndex), 4868 attribution: rec.attribution 4869 })); 4870 } 4871 } 4872 if (widgets?.positions?.length && widgets?.data?.length) { 4873 let positionIndex = 0; 4874 const source = "CARDGRID_WIDGET"; 4875 for (const widget of widgets.data) { 4876 let widgetComponent = null; 4877 const position = widgets.positions[positionIndex]; 4878 4879 // Stop if we run out of positions to place widgets. 4880 if (!position) { 4881 break; 4882 } 4883 switch (widget?.type) { 4884 case "TopicsWidget": 4885 widgetComponent = /*#__PURE__*/external_React_default().createElement(TopicsWidget, { 4886 position: position.index, 4887 dispatch: this.props.dispatch, 4888 source: source, 4889 id: WIDGET_IDS.TOPICS 4890 }); 4891 break; 4892 } 4893 if (widgetComponent) { 4894 // We found a widget, so up the position for next try. 4895 positionIndex++; 4896 // We replace an existing card with the widget. 4897 cards.splice(position.index, 1, widgetComponent); 4898 } 4899 } 4900 } 4901 4902 // if a banner ad is enabled and we have any available, place them in the grid 4903 const { 4904 spocs 4905 } = this.props.DiscoveryStream; 4906 if ((billboardEnabled || leaderboardEnabled) && spocs?.data?.newtab_spocs?.items) { 4907 // Only render one AdBanner in the grid - 4908 // Prioritize rendering a leaderboard if it exists, 4909 // otherwise render a billboard 4910 const spocToRender = spocs.data.newtab_spocs.items.find(({ 4911 format 4912 }) => format === "leaderboard" && leaderboardEnabled) || spocs.data.newtab_spocs.items.find(({ 4913 format 4914 }) => format === "billboard" && billboardEnabled); 4915 if (spocToRender && !spocs.blocked.includes(spocToRender.url)) { 4916 const row = spocToRender.format === "leaderboard" ? prefs[PREF_LEADERBOARD_POSITION] : prefs[PREF_BILLBOARD_POSITION]; 4917 function displayCardsPerRow() { 4918 // Determines the number of cards per row based on the window width: 4919 // width <= 1122px: 2 cards per row 4920 // width 1123px to 1697px: 3 cards per row 4921 // width >= 1698px: 4 cards per row 4922 if (window.innerWidth <= 1122) { 4923 return 2; 4924 } else if (window.innerWidth > 1122 && window.innerWidth < 1698) { 4925 return 3; 4926 } 4927 return 4; 4928 } 4929 const injectAdBanner = bannerIndex => { 4930 // .splice() inserts the AdBanner at the desired index, ensuring correct DOM order for accessibility and keyboard navigation. 4931 // .push() would place it at the end, which is visually incorrect even if adjusted with CSS. 4932 cards.splice(bannerIndex, 0, /*#__PURE__*/external_React_default().createElement(AdBanner, { 4933 spoc: spocToRender, 4934 key: `dscard-${spocToRender.id}`, 4935 dispatch: this.props.dispatch, 4936 type: this.props.type, 4937 firstVisibleTimestamp: this.props.firstVisibleTimestamp, 4938 row: row, 4939 prefs: prefs 4940 })); 4941 }; 4942 const getBannerIndex = () => { 4943 // Calculate the index for where the AdBanner should be added, depending on number of cards per row on the grid 4944 const cardsPerRow = displayCardsPerRow(); 4945 let bannerIndex = (row - 1) * cardsPerRow; 4946 return bannerIndex; 4947 }; 4948 injectAdBanner(getBannerIndex()); 4949 } 4950 } 4951 const gridClassName = this.renderGridClassName(); 4952 return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, cards?.length > 0 && /*#__PURE__*/external_React_default().createElement("div", { 4953 className: gridClassName, 4954 onKeyDown: this.handleCardKeyDown 4955 }, cards)); 4956 } 4957 renderGridClassName() { 4958 const { 4959 hybridLayout, 4960 hideCardBackground, 4961 fourCardLayout, 4962 compactGrid, 4963 hideDescriptions 4964 } = this.props; 4965 const hideCardBackgroundClass = hideCardBackground ? `ds-card-grid-hide-background` : ``; 4966 const fourCardLayoutClass = fourCardLayout ? `ds-card-grid-four-card-variant` : ``; 4967 const hideDescriptionsClassName = !hideDescriptions ? `ds-card-grid-include-descriptions` : ``; 4968 const compactGridClassName = compactGrid ? `ds-card-grid-compact` : ``; 4969 const hybridLayoutClassName = hybridLayout ? `ds-card-grid-hybrid-layout` : ``; 4970 const gridClassName = `ds-card-grid ${hybridLayoutClassName} ${hideCardBackgroundClass} ${fourCardLayoutClass} ${hideDescriptionsClassName} ${compactGridClassName}`; 4971 return gridClassName; 4972 } 4973 render() { 4974 const { 4975 data 4976 } = this.props; 4977 4978 // Handle a render before feed has been fetched by displaying nothing 4979 if (!data) { 4980 return null; 4981 } 4982 4983 // Handle the case where a user has dismissed all recommendations 4984 const isEmpty = data.recommendations.length === 0; 4985 return /*#__PURE__*/external_React_default().createElement("div", null, this.props.title && /*#__PURE__*/external_React_default().createElement("div", { 4986 className: "ds-header" 4987 }, /*#__PURE__*/external_React_default().createElement("div", { 4988 className: "title" 4989 }, this.props.title), this.props.context && /*#__PURE__*/external_React_default().createElement(FluentOrText, { 4990 message: this.props.context 4991 }, /*#__PURE__*/external_React_default().createElement("div", { 4992 className: "ds-context" 4993 }))), isEmpty ? /*#__PURE__*/external_React_default().createElement("div", { 4994 className: "ds-card-grid empty" 4995 }, /*#__PURE__*/external_React_default().createElement(DSEmptyState, { 4996 status: data.status, 4997 dispatch: this.props.dispatch, 4998 feed: this.props.feed 4999 })) : this.renderCards()); 5000 } 5001 } 5002 _CardGrid.defaultProps = { 5003 items: 4 // Number of stories to display 5004 }; 5005 const CardGrid = (0,external_ReactRedux_namespaceObject.connect)(state => ({ 5006 Prefs: state.Prefs, 5007 App: state.App, 5008 DiscoveryStream: state.DiscoveryStream 5009 }))(_CardGrid); 5010 ;// CONCATENATED MODULE: ./content-src/components/A11yLinkButton/A11yLinkButton.jsx 5011 function A11yLinkButton_extends() { return A11yLinkButton_extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, A11yLinkButton_extends.apply(null, arguments); } 5012 /* This Source Code Form is subject to the terms of the Mozilla Public 5013 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5014 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 5015 5016 5017 function A11yLinkButton(props) { 5018 // function for merging classes, if necessary 5019 let className = "a11y-link-button"; 5020 if (props.className) { 5021 className += ` ${props.className}`; 5022 } 5023 return /*#__PURE__*/external_React_default().createElement("button", A11yLinkButton_extends({ 5024 type: "button" 5025 }, props, { 5026 className: className 5027 }), props.children); 5028 } 5029 ;// CONCATENATED MODULE: ./content-src/components/ErrorBoundary/ErrorBoundary.jsx 5030 /* This Source Code Form is subject to the terms of the Mozilla Public 5031 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5032 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 5033 5034 5035 5036 class ErrorBoundaryFallback extends (external_React_default()).PureComponent { 5037 constructor(props) { 5038 super(props); 5039 this.windowObj = this.props.windowObj || window; 5040 this.onClick = this.onClick.bind(this); 5041 } 5042 5043 /** 5044 * Since we only get here if part of the page has crashed, do a 5045 * forced reload to give us the best chance at recovering. 5046 */ 5047 onClick() { 5048 this.windowObj.location.reload(true); 5049 } 5050 render() { 5051 const defaultClass = "as-error-fallback"; 5052 let className; 5053 if ("className" in this.props) { 5054 className = `${this.props.className} ${defaultClass}`; 5055 } else { 5056 className = defaultClass; 5057 } 5058 5059 // "A11yLinkButton" to force normal link styling stuff (eg cursor on hover) 5060 return /*#__PURE__*/external_React_default().createElement("div", { 5061 className: className 5062 }, /*#__PURE__*/external_React_default().createElement("div", { 5063 "data-l10n-id": "newtab-error-fallback-info" 5064 }), /*#__PURE__*/external_React_default().createElement("span", null, /*#__PURE__*/external_React_default().createElement(A11yLinkButton, { 5065 className: "reload-button", 5066 onClick: this.onClick, 5067 "data-l10n-id": "newtab-error-fallback-refresh-link" 5068 }))); 5069 } 5070 } 5071 ErrorBoundaryFallback.defaultProps = { 5072 className: "as-error-fallback" 5073 }; 5074 class ErrorBoundary extends (external_React_default()).PureComponent { 5075 constructor(props) { 5076 super(props); 5077 this.state = { 5078 hasError: false 5079 }; 5080 } 5081 componentDidCatch() { 5082 this.setState({ 5083 hasError: true 5084 }); 5085 } 5086 render() { 5087 if (!this.state.hasError) { 5088 return this.props.children; 5089 } 5090 return /*#__PURE__*/external_React_default().createElement(this.props.FallbackComponent, { 5091 className: this.props.className 5092 }); 5093 } 5094 } 5095 ErrorBoundary.defaultProps = { 5096 FallbackComponent: ErrorBoundaryFallback 5097 }; 5098 ;// CONCATENATED MODULE: ./content-src/components/CollapsibleSection/CollapsibleSection.jsx 5099 /* This Source Code Form is subject to the terms of the Mozilla Public 5100 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5101 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 5102 5103 5104 5105 5106 5107 5108 5109 /** 5110 * A section that can collapse. As of bug 1710937, it can no longer collapse. 5111 * See bug 1727365 for follow-up work to simplify this component. 5112 */ 5113 class _CollapsibleSection extends (external_React_default()).PureComponent { 5114 constructor(props) { 5115 super(props); 5116 this.onBodyMount = this.onBodyMount.bind(this); 5117 this.onMenuButtonMouseEnter = this.onMenuButtonMouseEnter.bind(this); 5118 this.onMenuButtonMouseLeave = this.onMenuButtonMouseLeave.bind(this); 5119 this.onMenuUpdate = this.onMenuUpdate.bind(this); 5120 this.setContextMenuButtonRef = this.setContextMenuButtonRef.bind(this); 5121 this.handleTopicSelectionButtonClick = this.handleTopicSelectionButtonClick.bind(this); 5122 this.state = { 5123 menuButtonHover: false, 5124 showContextMenu: false 5125 }; 5126 } 5127 setContextMenuButtonRef(element) { 5128 this.contextMenuButtonRef = element; 5129 } 5130 onBodyMount(node) { 5131 this.sectionBody = node; 5132 } 5133 onMenuButtonMouseEnter() { 5134 this.setState({ 5135 menuButtonHover: true 5136 }); 5137 } 5138 onMenuButtonMouseLeave() { 5139 this.setState({ 5140 menuButtonHover: false 5141 }); 5142 } 5143 onMenuUpdate(showContextMenu) { 5144 this.setState({ 5145 showContextMenu 5146 }); 5147 } 5148 handleTopicSelectionButtonClick() { 5149 const maybeDisplay = this.props.Prefs.values["discoverystream.topicSelection.onboarding.maybeDisplay"]; 5150 this.props.dispatch(actionCreators.OnlyToMain({ 5151 type: actionTypes.TOPIC_SELECTION_USER_OPEN 5152 })); 5153 if (maybeDisplay) { 5154 // if still part of onboarding, remove user from onboarding flow 5155 this.props.dispatch(actionCreators.SetPref("discoverystream.topicSelection.onboarding.maybeDisplay", false)); 5156 } 5157 this.props.dispatch(actionCreators.BroadcastToContent({ 5158 type: actionTypes.TOPIC_SELECTION_SPOTLIGHT_OPEN 5159 })); 5160 } 5161 render() { 5162 const { 5163 isAnimating, 5164 maxHeight, 5165 menuButtonHover, 5166 showContextMenu 5167 } = this.state; 5168 const { 5169 id, 5170 collapsed, 5171 title, 5172 subTitle, 5173 mayHaveTopicsSelection, 5174 sectionsEnabled 5175 } = this.props; 5176 const active = menuButtonHover || showContextMenu; 5177 let bodyStyle; 5178 if (isAnimating && !collapsed) { 5179 bodyStyle = { 5180 maxHeight 5181 }; 5182 } else if (!isAnimating && collapsed) { 5183 bodyStyle = { 5184 display: "none" 5185 }; 5186 } 5187 let titleStyle; 5188 if (this.props.hideTitle) { 5189 titleStyle = { 5190 visibility: "hidden" 5191 }; 5192 } 5193 const hasSubtitleClassName = subTitle ? `has-subtitle` : ``; 5194 const hasBeenUpdatedPreviously = this.props.Prefs.values["discoverystream.topicSelection.hasBeenUpdatedPreviously"]; 5195 const selectedTopics = this.props.Prefs.values["discoverystream.topicSelection.selectedTopics"]; 5196 const topicsHaveBeenPreviouslySet = hasBeenUpdatedPreviously || selectedTopics; 5197 return /*#__PURE__*/external_React_default().createElement("section", { 5198 className: `collapsible-section ${this.props.className}${active ? " active" : ""}` 5199 // Note: data-section-id is used for web extension api tests in mozilla central 5200 , 5201 "data-section-id": id 5202 }, !sectionsEnabled && /*#__PURE__*/external_React_default().createElement("div", { 5203 className: "section-top-bar" 5204 }, /*#__PURE__*/external_React_default().createElement("h2", { 5205 className: `section-title-container ${hasSubtitleClassName}`, 5206 style: titleStyle 5207 }, /*#__PURE__*/external_React_default().createElement("span", { 5208 className: "section-title" 5209 }, /*#__PURE__*/external_React_default().createElement(FluentOrText, { 5210 message: title 5211 })), subTitle && /*#__PURE__*/external_React_default().createElement("span", { 5212 className: "section-sub-title" 5213 }, /*#__PURE__*/external_React_default().createElement(FluentOrText, { 5214 message: subTitle 5215 }))), mayHaveTopicsSelection && /*#__PURE__*/external_React_default().createElement("div", { 5216 className: "button-topic-selection" 5217 }, /*#__PURE__*/external_React_default().createElement("moz-button", { 5218 "data-l10n-id": topicsHaveBeenPreviouslySet ? "newtab-topic-selection-button-update-interests" : "newtab-topic-selection-button-pick-interests", 5219 type: topicsHaveBeenPreviouslySet ? "default" : "primary", 5220 onClick: this.handleTopicSelectionButtonClick 5221 }))), /*#__PURE__*/external_React_default().createElement(ErrorBoundary, { 5222 className: "section-body-fallback" 5223 }, /*#__PURE__*/external_React_default().createElement("div", { 5224 ref: this.onBodyMount, 5225 style: bodyStyle 5226 }, this.props.children))); 5227 } 5228 } 5229 _CollapsibleSection.defaultProps = { 5230 document: globalThis.document || { 5231 addEventListener: () => {}, 5232 removeEventListener: () => {}, 5233 visibilityState: "hidden" 5234 } 5235 }; 5236 const CollapsibleSection = (0,external_ReactRedux_namespaceObject.connect)(state => ({ 5237 Prefs: state.Prefs 5238 }))(_CollapsibleSection); 5239 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSMessage/DSMessage.jsx 5240 /* This Source Code Form is subject to the terms of the Mozilla Public 5241 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5242 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 5243 5244 5245 5246 5247 class DSMessage extends (external_React_default()).PureComponent { 5248 render() { 5249 return /*#__PURE__*/external_React_default().createElement("div", { 5250 className: "ds-message" 5251 }, /*#__PURE__*/external_React_default().createElement("header", { 5252 className: "title" 5253 }, this.props.icon && /*#__PURE__*/external_React_default().createElement("div", { 5254 className: "glyph", 5255 style: { 5256 backgroundImage: `url(${this.props.icon})` 5257 } 5258 }), this.props.title && /*#__PURE__*/external_React_default().createElement("span", { 5259 className: "title-text" 5260 }, /*#__PURE__*/external_React_default().createElement(FluentOrText, { 5261 message: this.props.title 5262 })), this.props.link_text && this.props.link_url && /*#__PURE__*/external_React_default().createElement(SafeAnchor, { 5263 className: "link", 5264 url: this.props.link_url 5265 }, /*#__PURE__*/external_React_default().createElement(FluentOrText, { 5266 message: this.props.link_text 5267 })))); 5268 } 5269 } 5270 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/ReportContent/ReportContent.jsx 5271 /* This Source Code Form is subject to the terms of the Mozilla Public 5272 * License, v. 2.0. If a copy of the MPL was not distributed with this 5273 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 5274 5275 5276 5277 const ReportContent = spocs => { 5278 const dispatch = (0,external_ReactRedux_namespaceObject.useDispatch)(); 5279 const modal = (0,external_React_namespaceObject.useRef)(null); 5280 const radioGroupRef = (0,external_React_namespaceObject.useRef)(null); 5281 const submitButtonRef = (0,external_React_namespaceObject.useRef)(null); 5282 const report = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.DiscoveryStream.report); 5283 const [valueSelected, setValueSelected] = (0,external_React_namespaceObject.useState)(false); 5284 const [selectedReason, setSelectedReason] = (0,external_React_namespaceObject.useState)(null); 5285 const spocData = spocs.spocs.data; 5286 5287 // Sends a dispatch to update the redux store when modal is cancelled 5288 const handleCancel = () => { 5289 dispatch(actionCreators.AlsoToMain({ 5290 type: actionTypes.REPORT_CLOSE 5291 })); 5292 }; 5293 const handleSubmit = (0,external_React_namespaceObject.useCallback)(() => { 5294 const { 5295 card_type, 5296 corpus_item_id, 5297 position, 5298 reporting_url, 5299 scheduled_corpus_item_id, 5300 section_position, 5301 section, 5302 title, 5303 topic, 5304 url 5305 } = report; 5306 if (card_type === "organic") { 5307 dispatch(actionCreators.AlsoToMain({ 5308 type: actionTypes.REPORT_CONTENT_SUBMIT, 5309 data: { 5310 card_type, 5311 corpus_item_id, 5312 report_reason: selectedReason, 5313 scheduled_corpus_item_id, 5314 section_position, 5315 section, 5316 title, 5317 topic, 5318 url 5319 } 5320 })); 5321 } else if (card_type === "spoc") { 5322 // Retrieve placement_id by comparing spocData with the ad that was reported 5323 const getPlacementId = () => { 5324 if (!spocData || !report.url) { 5325 return null; 5326 } 5327 for (const [placementId, spocList] of Object.entries(spocData)) { 5328 for (const spoc of Object.values(spocList)) { 5329 if (spoc?.url === report.url) { 5330 return placementId; 5331 } 5332 } 5333 } 5334 return null; 5335 }; 5336 const placement_id = getPlacementId(); 5337 dispatch(actionCreators.AlsoToMain({ 5338 type: actionTypes.REPORT_AD_SUBMIT, 5339 data: { 5340 report_reason: selectedReason, 5341 placement_id, 5342 position, 5343 reporting_url, 5344 url 5345 } 5346 })); 5347 } 5348 dispatch(actionCreators.AlsoToMain({ 5349 type: actionTypes.BLOCK_URL, 5350 data: [{ 5351 ...report 5352 }] 5353 })); 5354 dispatch(actionCreators.OnlyToOneContent({ 5355 type: actionTypes.SHOW_TOAST_MESSAGE, 5356 data: { 5357 toastId: "reportSuccessToast", 5358 showNotifications: true 5359 } 5360 }, "ActivityStream:Content")); 5361 }, [dispatch, selectedReason, report, spocData]); 5362 5363 // Opens and closes the modal based on user interaction 5364 (0,external_React_namespaceObject.useEffect)(() => { 5365 if (report.visible && modal?.current) { 5366 modal.current.showModal(); 5367 5368 // Clear any previously selected radio button 5369 const radioGroup = radioGroupRef.current; 5370 if (radioGroup) { 5371 const selectedRadioButton = radioGroup.querySelector("moz-radio[checked]"); 5372 if (selectedRadioButton) { 5373 selectedRadioButton.removeAttribute("checked"); 5374 } 5375 } 5376 5377 // Clear out the states 5378 setValueSelected(false); 5379 setSelectedReason(null); 5380 } else if (!report.visible && modal?.current?.open) { 5381 modal.current.close(); 5382 } 5383 }, [report.visible]); 5384 5385 // Updates the submit button's state based on if a value is selected 5386 (0,external_React_namespaceObject.useEffect)(() => { 5387 const radioGroup = radioGroupRef.current; 5388 const submitButton = submitButtonRef.current; 5389 const handleRadioChange = e => { 5390 const reasonValue = e?.target?.value; 5391 if (reasonValue) { 5392 setValueSelected(true); 5393 setSelectedReason(reasonValue); 5394 } 5395 }; 5396 if (radioGroup) { 5397 radioGroup.addEventListener("change", handleRadioChange); 5398 } 5399 5400 // Handle submit button state on valueSelected change 5401 const updateSubmitState = () => { 5402 if (valueSelected) { 5403 submitButton.removeAttribute("disabled"); 5404 } else { 5405 submitButton.setAttribute("disabled", ""); 5406 } 5407 }; 5408 updateSubmitState(); 5409 return () => { 5410 if (radioGroup) { 5411 radioGroup.removeEventListener("change", handleRadioChange); 5412 } 5413 }; 5414 }, [valueSelected, selectedReason]); 5415 return /*#__PURE__*/external_React_default().createElement("dialog", { 5416 className: "report-content-form", 5417 id: "dialog-report", 5418 ref: modal, 5419 onClose: () => dispatch({ 5420 type: actionTypes.REPORT_CLOSE 5421 }) 5422 }, /*#__PURE__*/external_React_default().createElement("form", { 5423 action: "" 5424 }, report.card_type === "spoc" ? /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("moz-radio-group", { 5425 name: "report", 5426 ref: radioGroupRef, 5427 id: "report-group", 5428 "data-l10n-id": "newtab-report-ads-why-reporting", 5429 className: "report-ads-options", 5430 headingLevel: "3" 5431 }, /*#__PURE__*/external_React_default().createElement("moz-radio", { 5432 "data-l10n-id": "newtab-report-ads-reason-not-interested", 5433 value: "not_interested" 5434 }), /*#__PURE__*/external_React_default().createElement("moz-radio", { 5435 "data-l10n-id": "newtab-report-ads-reason-inappropriate", 5436 value: "inappropriate" 5437 }), /*#__PURE__*/external_React_default().createElement("moz-radio", { 5438 "data-l10n-id": "newtab-report-ads-reason-seen-it-too-many-times", 5439 value: "seen_too_many_times" 5440 }))) : /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("moz-radio-group", { 5441 name: "report", 5442 ref: radioGroupRef, 5443 id: "report-group", 5444 "data-l10n-id": "newtab-report-content-why-reporting-this", 5445 className: "report-content-options", 5446 headingLevel: "3" 5447 }, /*#__PURE__*/external_React_default().createElement("moz-radio", { 5448 "data-l10n-id": "newtab-report-content-wrong-category", 5449 value: "wrong_category" 5450 }), /*#__PURE__*/external_React_default().createElement("moz-radio", { 5451 "data-l10n-id": "newtab-report-content-outdated", 5452 value: "outdated" 5453 }), /*#__PURE__*/external_React_default().createElement("moz-radio", { 5454 "data-l10n-id": "newtab-report-content-inappropriate-offensive", 5455 value: "inappropriate_or_offensive" 5456 }), /*#__PURE__*/external_React_default().createElement("moz-radio", { 5457 "data-l10n-id": "newtab-report-content-spam-misleading", 5458 value: "spam_or_misleading" 5459 }), /*#__PURE__*/external_React_default().createElement("moz-radio", { 5460 "data-l10n-id": "newtab-report-content-requires-payment-subscription", 5461 value: "requires_payment_or_subscription" 5462 }, /*#__PURE__*/external_React_default().createElement("a", { 5463 slot: "support-link", 5464 is: "moz-support-link", 5465 "support-page": "recommendations-firefox-new-tab#w_what-is-a-paywall", 5466 "data-l10n-id": "newtab-report-content-requires-payment-subscription-learn-more", 5467 rel: "noreferrer", 5468 target: "_blank" 5469 })))), /*#__PURE__*/external_React_default().createElement("moz-button-group", null, /*#__PURE__*/external_React_default().createElement("moz-button", { 5470 "data-l10n-id": "newtab-report-cancel", 5471 onClick: handleCancel, 5472 className: "cancel-report-btn" 5473 }), /*#__PURE__*/external_React_default().createElement("moz-button", { 5474 type: "primary", 5475 "data-l10n-id": "newtab-report-submit", 5476 ref: submitButtonRef, 5477 onClick: handleSubmit, 5478 className: "submit-report-btn" 5479 })))); 5480 }; 5481 ;// CONCATENATED MODULE: ./content-src/lib/screenshot-utils.mjs 5482 /* This Source Code Form is subject to the terms of the Mozilla Public 5483 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5484 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 5485 5486 /** 5487 * List of helper functions for screenshot-based images. 5488 * 5489 * There are two kinds of images: 5490 * 1. Remote Image: This is the image from the main process and it refers to 5491 * the image in the React props. This can either be an object with the `data` 5492 * and `path` properties, if it is a blob, or a string, if it is a normal image. 5493 * 2. Local Image: This is the image object in the content process and it refers 5494 * to the image *object* in the React component's state. All local image 5495 * objects have the `url` property, and an additional property `path`, if they 5496 * are blobs. 5497 */ 5498 const ScreenshotUtils = { 5499 isBlob(isLocal, image) { 5500 return !!( 5501 image && 5502 image.path && 5503 ((!isLocal && image.data) || (isLocal && image.url)) 5504 ); 5505 }, 5506 5507 // This should always be called with a remote image and not a local image. 5508 createLocalImageObject(remoteImage) { 5509 if (!remoteImage) { 5510 return null; 5511 } 5512 if (this.isBlob(false, remoteImage)) { 5513 return { 5514 url: globalThis.URL.createObjectURL(remoteImage.data), 5515 path: remoteImage.path, 5516 }; 5517 } 5518 return { url: remoteImage }; 5519 }, 5520 5521 // Revokes the object URL of the image if the local image is a blob. 5522 // This should always be called with a local image and not a remote image. 5523 maybeRevokeBlobObjectURL(localImage) { 5524 if (this.isBlob(true, localImage)) { 5525 globalThis.URL.revokeObjectURL(localImage.url); 5526 } 5527 }, 5528 5529 // Checks if remoteImage and localImage are the same. 5530 isRemoteImageLocal(localImage, remoteImage) { 5531 // Both remoteImage and localImage are present. 5532 if (remoteImage && localImage) { 5533 return this.isBlob(false, remoteImage) 5534 ? localImage.path === remoteImage.path 5535 : localImage.url === remoteImage; 5536 } 5537 5538 // This will only handle the remaining three possible outcomes. 5539 // (i.e. everything except when both image and localImage are present) 5540 return !remoteImage && !localImage; 5541 }, 5542 }; 5543 5544 ;// CONCATENATED MODULE: ./content-src/components/Card/Card.jsx 5545 /* This Source Code Form is subject to the terms of the Mozilla Public 5546 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5547 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 5548 5549 5550 5551 5552 5553 5554 5555 5556 5557 // Keep track of pending image loads to only request once 5558 const gImageLoading = new Map(); 5559 5560 /** 5561 * Card component. 5562 * Cards are found within a Section component and contain information about a link such 5563 * as preview image, page title, page description, and some context about if the page 5564 * was visited, bookmarked, trending etc... 5565 * Each Section can make an unordered list of Cards which will create one instane of 5566 * this class. Each card will then get a context menu which reflects the actions that 5567 * can be done on this Card. 5568 */ 5569 class _Card extends (external_React_default()).PureComponent { 5570 constructor(props) { 5571 super(props); 5572 this.state = { 5573 activeCard: null, 5574 imageLoaded: false, 5575 cardImage: null 5576 }; 5577 this.onMenuButtonUpdate = this.onMenuButtonUpdate.bind(this); 5578 this.onLinkClick = this.onLinkClick.bind(this); 5579 } 5580 5581 /** 5582 * Helper to conditionally load an image and update state when it loads. 5583 */ 5584 async maybeLoadImage() { 5585 // No need to load if it's already loaded or no image 5586 const { 5587 cardImage 5588 } = this.state; 5589 if (!cardImage) { 5590 return; 5591 } 5592 const imageUrl = cardImage.url; 5593 if (!this.state.imageLoaded) { 5594 // Initialize a promise to share a load across multiple card updates 5595 if (!gImageLoading.has(imageUrl)) { 5596 const loaderPromise = new Promise((resolve, reject) => { 5597 const loader = new Image(); 5598 loader.addEventListener("load", resolve); 5599 loader.addEventListener("error", reject); 5600 loader.src = imageUrl; 5601 }); 5602 5603 // Save and remove the promise only while it's pending 5604 gImageLoading.set(imageUrl, loaderPromise); 5605 loaderPromise.catch(ex => ex).then(() => gImageLoading.delete(imageUrl)); 5606 } 5607 5608 // Wait for the image whether just started loading or reused promise 5609 try { 5610 await gImageLoading.get(imageUrl); 5611 } catch (ex) { 5612 // Ignore the failed image without changing state 5613 return; 5614 } 5615 5616 // Only update state if we're still waiting to load the original image 5617 if (ScreenshotUtils.isRemoteImageLocal(this.state.cardImage, this.props.link.image) && !this.state.imageLoaded) { 5618 this.setState({ 5619 imageLoaded: true 5620 }); 5621 } 5622 } 5623 } 5624 5625 /** 5626 * Helper to obtain the next state based on nextProps and prevState. 5627 * 5628 * NOTE: Rename this method to getDerivedStateFromProps when we update React 5629 * to >= 16.3. We will need to update tests as well. We cannot rename this 5630 * method to getDerivedStateFromProps now because there is a mismatch in 5631 * the React version that we are using for both testing and production. 5632 * (i.e. react-test-render => "16.3.2", react => "16.2.0"). 5633 * 5634 * See https://github.com/airbnb/enzyme/blob/master/packages/enzyme-adapter-react-16/package.json#L43. 5635 */ 5636 static getNextStateFromProps(nextProps, prevState) { 5637 const { 5638 image 5639 } = nextProps.link; 5640 const imageInState = ScreenshotUtils.isRemoteImageLocal(prevState.cardImage, image); 5641 let nextState = null; 5642 5643 // Image is updating. 5644 if (!imageInState && nextProps.link) { 5645 nextState = { 5646 imageLoaded: false 5647 }; 5648 } 5649 if (imageInState) { 5650 return nextState; 5651 } 5652 5653 // Since image was updated, attempt to revoke old image blob URL, if it exists. 5654 ScreenshotUtils.maybeRevokeBlobObjectURL(prevState.cardImage); 5655 nextState = nextState || {}; 5656 nextState.cardImage = ScreenshotUtils.createLocalImageObject(image); 5657 return nextState; 5658 } 5659 onMenuButtonUpdate(isOpen) { 5660 if (isOpen) { 5661 this.setState({ 5662 activeCard: this.props.index 5663 }); 5664 } else { 5665 this.setState({ 5666 activeCard: null 5667 }); 5668 } 5669 } 5670 5671 /** 5672 * Report to telemetry additional information about the item. 5673 */ 5674 _getTelemetryInfo() { 5675 // Filter out "history" type for being the default 5676 if (this.props.link.type !== "history") { 5677 return { 5678 value: { 5679 card_type: this.props.link.type 5680 } 5681 }; 5682 } 5683 return null; 5684 } 5685 onLinkClick(event) { 5686 event.preventDefault(); 5687 const { 5688 altKey, 5689 button, 5690 ctrlKey, 5691 metaKey, 5692 shiftKey 5693 } = event; 5694 if (this.props.link.type === "download") { 5695 this.props.dispatch(actionCreators.OnlyToMain({ 5696 type: actionTypes.OPEN_DOWNLOAD_FILE, 5697 data: Object.assign(this.props.link, { 5698 event: { 5699 button, 5700 ctrlKey, 5701 metaKey, 5702 shiftKey 5703 } 5704 }) 5705 })); 5706 } else { 5707 this.props.dispatch(actionCreators.OnlyToMain({ 5708 type: actionTypes.OPEN_LINK, 5709 data: Object.assign(this.props.link, { 5710 event: { 5711 altKey, 5712 button, 5713 ctrlKey, 5714 metaKey, 5715 shiftKey 5716 } 5717 }) 5718 })); 5719 } 5720 if (this.props.isWebExtension) { 5721 this.props.dispatch(actionCreators.WebExtEvent(actionTypes.WEBEXT_CLICK, { 5722 source: this.props.eventSource, 5723 url: this.props.link.url, 5724 action_position: this.props.index 5725 })); 5726 } else { 5727 this.props.dispatch(actionCreators.UserEvent(Object.assign({ 5728 event: "CLICK", 5729 source: this.props.eventSource, 5730 action_position: this.props.index 5731 }, this._getTelemetryInfo()))); 5732 if (this.props.shouldSendImpressionStats) { 5733 this.props.dispatch(actionCreators.ImpressionStats({ 5734 source: this.props.eventSource, 5735 click: 0, 5736 tiles: [{ 5737 id: this.props.link.guid, 5738 pos: this.props.index 5739 }] 5740 })); 5741 } 5742 } 5743 } 5744 componentDidMount() { 5745 this.maybeLoadImage(); 5746 } 5747 componentDidUpdate() { 5748 this.maybeLoadImage(); 5749 } 5750 5751 // NOTE: Remove this function when we update React to >= 16.3 since React will 5752 // call getDerivedStateFromProps automatically. We will also need to 5753 // rename getNextStateFromProps to getDerivedStateFromProps. 5754 componentWillMount() { 5755 const nextState = _Card.getNextStateFromProps(this.props, this.state); 5756 if (nextState) { 5757 this.setState(nextState); 5758 } 5759 } 5760 5761 // NOTE: Remove this function when we update React to >= 16.3 since React will 5762 // call getDerivedStateFromProps automatically. We will also need to 5763 // rename getNextStateFromProps to getDerivedStateFromProps. 5764 componentWillReceiveProps(nextProps) { 5765 const nextState = _Card.getNextStateFromProps(nextProps, this.state); 5766 if (nextState) { 5767 this.setState(nextState); 5768 } 5769 } 5770 componentWillUnmount() { 5771 ScreenshotUtils.maybeRevokeBlobObjectURL(this.state.cardImage); 5772 } 5773 render() { 5774 const { 5775 index, 5776 className, 5777 link, 5778 dispatch, 5779 contextMenuOptions, 5780 eventSource, 5781 shouldSendImpressionStats 5782 } = this.props; 5783 const { 5784 props 5785 } = this; 5786 const title = link.title || link.hostname; 5787 const isContextMenuOpen = this.state.activeCard === index; 5788 // Display "now" as "trending" until we have new strings #3402 5789 const { 5790 icon, 5791 fluentID 5792 } = cardContextTypes[link.type === "now" ? "trending" : link.type] || {}; 5793 const hasImage = this.state.cardImage || link.hasImage; 5794 const imageStyle = { 5795 backgroundImage: this.state.cardImage ? `url(${this.state.cardImage.url})` : "none" 5796 }; 5797 const outerClassName = ["card-outer", className, isContextMenuOpen && "active", props.placeholder && "placeholder"].filter(v => v).join(" "); 5798 return /*#__PURE__*/external_React_default().createElement("li", { 5799 className: outerClassName 5800 }, /*#__PURE__*/external_React_default().createElement("a", { 5801 href: link.type === "pocket" ? link.open_url : link.url, 5802 onClick: !props.placeholder ? this.onLinkClick : undefined 5803 }, /*#__PURE__*/external_React_default().createElement("div", { 5804 className: "card" 5805 }, /*#__PURE__*/external_React_default().createElement("div", { 5806 className: "card-preview-image-outer" 5807 }, hasImage && /*#__PURE__*/external_React_default().createElement("div", { 5808 className: `card-preview-image${this.state.imageLoaded ? " loaded" : ""}`, 5809 style: imageStyle 5810 })), /*#__PURE__*/external_React_default().createElement("div", { 5811 className: "card-details" 5812 }, link.type === "download" && /*#__PURE__*/external_React_default().createElement("div", { 5813 className: "card-host-name alternate", 5814 "data-l10n-id": "newtab-menu-open-file" 5815 }), link.hostname && /*#__PURE__*/external_React_default().createElement("div", { 5816 className: "card-host-name" 5817 }, link.hostname.slice(0, 100), link.type === "download" && ` \u2014 ${link.description}`), /*#__PURE__*/external_React_default().createElement("div", { 5818 className: ["card-text", icon ? "" : "no-context", link.description ? "" : "no-description", link.hostname ? "" : "no-host-name"].join(" ") 5819 }, /*#__PURE__*/external_React_default().createElement("h4", { 5820 className: "card-title", 5821 dir: "auto" 5822 }, link.title), /*#__PURE__*/external_React_default().createElement("p", { 5823 className: "card-description", 5824 dir: "auto" 5825 }, link.description)), /*#__PURE__*/external_React_default().createElement("div", { 5826 className: "card-context" 5827 }, icon && !link.context && /*#__PURE__*/external_React_default().createElement("span", { 5828 "aria-haspopup": "true", 5829 className: `card-context-icon icon icon-${icon}` 5830 }), link.icon && link.context && /*#__PURE__*/external_React_default().createElement("span", { 5831 "aria-haspopup": "true", 5832 className: "card-context-icon icon", 5833 style: { 5834 backgroundImage: `url('${link.icon}')` 5835 } 5836 }), fluentID && !link.context && /*#__PURE__*/external_React_default().createElement("div", { 5837 className: "card-context-label", 5838 "data-l10n-id": fluentID 5839 }), link.context && /*#__PURE__*/external_React_default().createElement("div", { 5840 className: "card-context-label" 5841 }, link.context))))), !props.placeholder && /*#__PURE__*/external_React_default().createElement(ContextMenuButton, { 5842 tooltip: "newtab-menu-content-tooltip", 5843 tooltipArgs: { 5844 title 5845 }, 5846 onUpdate: this.onMenuButtonUpdate 5847 }, /*#__PURE__*/external_React_default().createElement(LinkMenu, { 5848 dispatch: dispatch, 5849 index: index, 5850 source: eventSource, 5851 options: link.contextMenuOptions || contextMenuOptions, 5852 site: link, 5853 siteInfo: this._getTelemetryInfo(), 5854 shouldSendImpressionStats: shouldSendImpressionStats 5855 }))); 5856 } 5857 } 5858 _Card.defaultProps = { 5859 link: {} 5860 }; 5861 const Card = (0,external_ReactRedux_namespaceObject.connect)(state => ({ 5862 platform: state.Prefs.values.platform 5863 }))(_Card); 5864 const PlaceholderCard = props => /*#__PURE__*/external_React_default().createElement(Card, { 5865 placeholder: true, 5866 className: props.className 5867 }); 5868 ;// CONCATENATED MODULE: ./content-src/lib/perf-service.mjs 5869 /* This Source Code Form is subject to the terms of the Mozilla Public 5870 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5871 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 5872 5873 let usablePerfObj = window.performance; 5874 5875 function _PerfService(options) { 5876 // For testing, so that we can use a fake Window.performance object with 5877 // known state. 5878 if (options && options.performanceObj) { 5879 this._perf = options.performanceObj; 5880 } else { 5881 this._perf = usablePerfObj; 5882 } 5883 } 5884 5885 _PerfService.prototype = { 5886 /** 5887 * Calls the underlying mark() method on the appropriate Window.performance 5888 * object to add a mark with the given name to the appropriate performance 5889 * timeline. 5890 * 5891 * @param {string} name the name to give the current mark 5892 * @return {void} 5893 */ 5894 mark: function mark(str) { 5895 this._perf.mark(str); 5896 }, 5897 5898 /** 5899 * Calls the underlying getEntriesByName on the appropriate Window.performance 5900 * object. 5901 * 5902 * @param {string} name 5903 * @param {string} type eg "mark" 5904 * @return {Array} Performance* objects 5905 */ 5906 getEntriesByName: function getEntriesByName(entryName, type) { 5907 return this._perf.getEntriesByName(entryName, type); 5908 }, 5909 5910 /** 5911 * The timeOrigin property from the appropriate performance object. 5912 * Used to ensure that timestamps from the add-on code and the content code 5913 * are comparable. 5914 * 5915 * Note: If this is called from a context without a window 5916 * (eg a JSM in chrome), it will return the timeOrigin of the XUL hidden 5917 * window, which appears to be the first created window (and thus 5918 * timeOrigin) in the browser. Note also, however, there is also a private 5919 * hidden window, presumably for private browsing, which appears to be 5920 * created dynamically later. Exactly how/when that shows up needs to be 5921 * investigated. 5922 * 5923 * @return {number} A double of milliseconds with a precision of 0.5us. 5924 */ 5925 get timeOrigin() { 5926 return this._perf.timeOrigin; 5927 }, 5928 5929 /** 5930 * Returns the "absolute" version of performance.now(), i.e. one that 5931 * should ([bug 1401406](https://bugzilla.mozilla.org/show_bug.cgi?id=1401406) 5932 * be comparable across both chrome and content. 5933 * 5934 * @return {number} 5935 */ 5936 absNow: function absNow() { 5937 return this.timeOrigin + this._perf.now(); 5938 }, 5939 5940 /** 5941 * This returns the absolute startTime from the most recent performance.mark() 5942 * with the given name. 5943 * 5944 * @param {string} name the name to lookup the start time for 5945 * 5946 * @return {number} the returned start time, as a DOMHighResTimeStamp 5947 * 5948 * @throws {Error} "No Marks with the name ..." if none are available 5949 * 5950 * Note: Always surround calls to this by try/catch. Otherwise your code 5951 * may fail when the `privacy.resistFingerprinting` pref is true. When 5952 * this pref is set, all attempts to get marks will likely fail, which will 5953 * cause this method to throw. 5954 * 5955 * See [bug 1369303](https://bugzilla.mozilla.org/show_bug.cgi?id=1369303) 5956 * for more info. 5957 */ 5958 getMostRecentAbsMarkStartByName(entryName) { 5959 let entries = this.getEntriesByName(entryName, "mark"); 5960 5961 if (!entries.length) { 5962 throw new Error(`No marks with the name ${entryName}`); 5963 } 5964 5965 let mostRecentEntry = entries[entries.length - 1]; 5966 return this._perf.timeOrigin + mostRecentEntry.startTime; 5967 }, 5968 }; 5969 5970 const perfService = new _PerfService(); 5971 5972 ;// CONCATENATED MODULE: ./content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx 5973 /* This Source Code Form is subject to the terms of the Mozilla Public 5974 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5975 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 5976 5977 5978 5979 5980 5981 // Currently record only a fixed set of sections. This will prevent data 5982 // from custom sections from showing up or from topstories. 5983 const RECORDED_SECTIONS = ["highlights", "topsites"]; 5984 class ComponentPerfTimer extends (external_React_default()).Component { 5985 constructor(props) { 5986 super(props); 5987 // Just for test dependency injection: 5988 this.perfSvc = this.props.perfSvc || perfService; 5989 this._sendBadStateEvent = this._sendBadStateEvent.bind(this); 5990 this._sendPaintedEvent = this._sendPaintedEvent.bind(this); 5991 this._reportMissingData = false; 5992 this._timestampHandled = false; 5993 this._recordedFirstRender = false; 5994 } 5995 componentDidMount() { 5996 if (!RECORDED_SECTIONS.includes(this.props.id)) { 5997 return; 5998 } 5999 this._maybeSendPaintedEvent(); 6000 } 6001 componentDidUpdate() { 6002 if (!RECORDED_SECTIONS.includes(this.props.id)) { 6003 return; 6004 } 6005 this._maybeSendPaintedEvent(); 6006 } 6007 6008 /** 6009 * Call the given callback after the upcoming frame paints. 6010 * 6011 * Note: Both setTimeout and requestAnimationFrame are throttled when the page 6012 * is hidden, so this callback may get called up to a second or so after the 6013 * requestAnimationFrame "paint" for hidden tabs. 6014 * 6015 * Newtabs hidden while loading will presumably be fairly rare (other than 6016 * preloaded tabs, which we will be filtering out on the server side), so such 6017 * cases should get lost in the noise. 6018 * 6019 * If we decide that it's important to find out when something that's hidden 6020 * has "painted", however, another option is to post a message to this window. 6021 * That should happen even faster than setTimeout, and, at least as of this 6022 * writing, it's not throttled in hidden windows in Firefox. 6023 * 6024 * @param {Function} callback 6025 * 6026 * @returns void 6027 */ 6028 _afterFramePaint(callback) { 6029 requestAnimationFrame(() => setTimeout(callback, 0)); 6030 } 6031 _maybeSendBadStateEvent() { 6032 // Follow up bugs: 6033 // https://github.com/mozilla/activity-stream/issues/3691 6034 if (!this.props.initialized) { 6035 // Remember to report back when data is available. 6036 this._reportMissingData = true; 6037 } else if (this._reportMissingData) { 6038 this._reportMissingData = false; 6039 // Report how long it took for component to become initialized. 6040 this._sendBadStateEvent(); 6041 } 6042 } 6043 _maybeSendPaintedEvent() { 6044 // If we've already handled a timestamp, don't do it again. 6045 if (this._timestampHandled || !this.props.initialized) { 6046 return; 6047 } 6048 6049 // And if we haven't, we're doing so now, so remember that. Even if 6050 // something goes wrong in the callback, we can't try again, as we'd be 6051 // sending back the wrong data, and we have to do it here, so that other 6052 // calls to this method while waiting for the next frame won't also try to 6053 // handle it. 6054 this._timestampHandled = true; 6055 this._afterFramePaint(this._sendPaintedEvent); 6056 } 6057 6058 /** 6059 * Triggered by call to render. Only first call goes through due to 6060 * `_recordedFirstRender`. 6061 */ 6062 _ensureFirstRenderTsRecorded() { 6063 // Used as t0 for recording how long component took to initialize. 6064 if (!this._recordedFirstRender) { 6065 this._recordedFirstRender = true; 6066 // topsites_first_render_ts, highlights_first_render_ts. 6067 const key = `${this.props.id}_first_render_ts`; 6068 this.perfSvc.mark(key); 6069 } 6070 } 6071 6072 /** 6073 * Creates `SAVE_SESSION_PERF_DATA` with timestamp in ms 6074 * of how much longer the data took to be ready for display than it would 6075 * have been the ideal case. 6076 * https://github.com/mozilla/ping-centre/issues/98 6077 */ 6078 _sendBadStateEvent() { 6079 // highlights_data_ready_ts, topsites_data_ready_ts. 6080 const dataReadyKey = `${this.props.id}_data_ready_ts`; 6081 this.perfSvc.mark(dataReadyKey); 6082 try { 6083 const firstRenderKey = `${this.props.id}_first_render_ts`; 6084 // value has to be Int32. 6085 const value = parseInt(this.perfSvc.getMostRecentAbsMarkStartByName(dataReadyKey) - this.perfSvc.getMostRecentAbsMarkStartByName(firstRenderKey), 10); 6086 this.props.dispatch(actionCreators.OnlyToMain({ 6087 type: actionTypes.SAVE_SESSION_PERF_DATA, 6088 // highlights_data_late_by_ms, topsites_data_late_by_ms. 6089 data: { 6090 [`${this.props.id}_data_late_by_ms`]: value 6091 } 6092 })); 6093 } catch (ex) { 6094 // If this failed, it's likely because the `privacy.resistFingerprinting` 6095 // pref is true. 6096 } 6097 } 6098 _sendPaintedEvent() { 6099 // Record first_painted event but only send if topsites. 6100 if (this.props.id !== "topsites") { 6101 return; 6102 } 6103 6104 // topsites_first_painted_ts. 6105 const key = `${this.props.id}_first_painted_ts`; 6106 this.perfSvc.mark(key); 6107 try { 6108 const data = {}; 6109 data[key] = this.perfSvc.getMostRecentAbsMarkStartByName(key); 6110 this.props.dispatch(actionCreators.OnlyToMain({ 6111 type: actionTypes.SAVE_SESSION_PERF_DATA, 6112 data 6113 })); 6114 } catch (ex) { 6115 // If this failed, it's likely because the `privacy.resistFingerprinting` 6116 // pref is true. We should at least not blow up, and should continue 6117 // to set this._timestampHandled to avoid going through this again. 6118 } 6119 } 6120 render() { 6121 if (RECORDED_SECTIONS.includes(this.props.id)) { 6122 this._ensureFirstRenderTsRecorded(); 6123 this._maybeSendBadStateEvent(); 6124 } 6125 return this.props.children; 6126 } 6127 } 6128 ;// CONCATENATED MODULE: ./content-src/components/MoreRecommendations/MoreRecommendations.jsx 6129 /* This Source Code Form is subject to the terms of the Mozilla Public 6130 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 6131 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 6132 6133 6134 class MoreRecommendations extends (external_React_default()).PureComponent { 6135 render() { 6136 const { 6137 read_more_endpoint 6138 } = this.props; 6139 if (read_more_endpoint) { 6140 return /*#__PURE__*/external_React_default().createElement("a", { 6141 className: "more-recommendations", 6142 href: read_more_endpoint, 6143 "data-l10n-id": "newtab-pocket-more-recommendations" 6144 }); 6145 } 6146 return null; 6147 } 6148 } 6149 ;// CONCATENATED MODULE: ./content-src/components/ModalOverlay/ModalOverlay.jsx 6150 /* This Source Code Form is subject to the terms of the Mozilla Public 6151 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 6152 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 6153 6154 6155 function ModalOverlayWrapper({ 6156 // eslint-disable-next-line no-shadow 6157 document = globalThis.document, 6158 unstyled, 6159 innerClassName, 6160 onClose, 6161 children, 6162 headerId, 6163 id 6164 }) { 6165 const modalRef = (0,external_React_namespaceObject.useRef)(null); 6166 let className = unstyled ? "" : "modalOverlayInner active"; 6167 if (innerClassName) { 6168 className += ` ${innerClassName}`; 6169 } 6170 6171 // The intended behaviour is to listen for an escape key 6172 // but not for a click; see Bug 1582242 6173 const onKeyDown = (0,external_React_namespaceObject.useCallback)(event => { 6174 if (event.key === "Escape") { 6175 onClose(event); 6176 } 6177 }, [onClose]); 6178 (0,external_React_namespaceObject.useEffect)(() => { 6179 document.addEventListener("keydown", onKeyDown); 6180 document.body.classList.add("modal-open"); 6181 return () => { 6182 document.removeEventListener("keydown", onKeyDown); 6183 document.body.classList.remove("modal-open"); 6184 }; 6185 }, [document, onKeyDown]); 6186 return /*#__PURE__*/external_React_default().createElement("div", { 6187 className: "modalOverlayOuter active", 6188 onKeyDown: onKeyDown, 6189 role: "presentation" 6190 }, /*#__PURE__*/external_React_default().createElement("div", { 6191 className: className, 6192 "aria-labelledby": headerId, 6193 id: id, 6194 role: "dialog", 6195 ref: modalRef 6196 }, children)); 6197 } 6198 6199 ;// CONCATENATED MODULE: ./content-src/components/TopSites/SearchShortcutsForm.jsx 6200 /* This Source Code Form is subject to the terms of the Mozilla Public 6201 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 6202 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 6203 6204 6205 6206 6207 class SelectableSearchShortcut extends (external_React_default()).PureComponent { 6208 render() { 6209 const { 6210 shortcut, 6211 selected 6212 } = this.props; 6213 const imageStyle = { 6214 backgroundImage: `url("${shortcut.tippyTopIcon}")` 6215 }; 6216 return /*#__PURE__*/external_React_default().createElement("div", { 6217 className: "top-site-outer search-shortcut" 6218 }, /*#__PURE__*/external_React_default().createElement("input", { 6219 type: "checkbox", 6220 id: shortcut.keyword, 6221 name: shortcut.keyword, 6222 checked: selected, 6223 onChange: this.props.onChange 6224 }), /*#__PURE__*/external_React_default().createElement("label", { 6225 htmlFor: shortcut.keyword 6226 }, /*#__PURE__*/external_React_default().createElement("div", { 6227 className: "top-site-inner" 6228 }, /*#__PURE__*/external_React_default().createElement("span", null, /*#__PURE__*/external_React_default().createElement("div", { 6229 className: "tile" 6230 }, /*#__PURE__*/external_React_default().createElement("div", { 6231 className: "top-site-icon rich-icon", 6232 style: imageStyle, 6233 "data-fallback": "@" 6234 }), /*#__PURE__*/external_React_default().createElement("div", { 6235 className: "top-site-icon search-topsite" 6236 })), /*#__PURE__*/external_React_default().createElement("div", { 6237 className: "title" 6238 }, /*#__PURE__*/external_React_default().createElement("span", { 6239 dir: "auto" 6240 }, shortcut.keyword)))))); 6241 } 6242 } 6243 class SearchShortcutsForm extends (external_React_default()).PureComponent { 6244 constructor(props) { 6245 super(props); 6246 this.handleChange = this.handleChange.bind(this); 6247 this.onCancelButtonClick = this.onCancelButtonClick.bind(this); 6248 this.onSaveButtonClick = this.onSaveButtonClick.bind(this); 6249 6250 // clone the shortcuts and add them to the state so we can add isSelected property 6251 const shortcuts = []; 6252 const { 6253 rows, 6254 searchShortcuts 6255 } = props.TopSites; 6256 searchShortcuts.forEach(shortcut => { 6257 shortcuts.push({ 6258 ...shortcut, 6259 isSelected: !!rows.find(row => row && row.isPinned && row.searchTopSite && row.label === shortcut.keyword) 6260 }); 6261 }); 6262 this.state = { 6263 shortcuts 6264 }; 6265 } 6266 handleChange(event) { 6267 const { 6268 target 6269 } = event; 6270 const { 6271 name: targetName, 6272 checked 6273 } = target; 6274 this.setState(prevState => { 6275 const shortcuts = prevState.shortcuts.slice(); 6276 let shortcut = shortcuts.find(({ 6277 keyword 6278 }) => keyword === targetName); 6279 shortcut.isSelected = checked; 6280 return { 6281 shortcuts 6282 }; 6283 }); 6284 } 6285 onCancelButtonClick(ev) { 6286 ev.preventDefault(); 6287 this.props.onClose(); 6288 } 6289 onSaveButtonClick(ev) { 6290 ev.preventDefault(); 6291 6292 // Check if there were any changes and act accordingly 6293 const { 6294 rows 6295 } = this.props.TopSites; 6296 const pinQueue = []; 6297 const unpinQueue = []; 6298 this.state.shortcuts.forEach(shortcut => { 6299 const alreadyPinned = rows.find(row => row && row.isPinned && row.searchTopSite && row.label === shortcut.keyword); 6300 if (shortcut.isSelected && !alreadyPinned) { 6301 pinQueue.push(this._searchTopSite(shortcut)); 6302 } else if (!shortcut.isSelected && alreadyPinned) { 6303 unpinQueue.push({ 6304 url: alreadyPinned.url, 6305 searchVendor: shortcut.shortURL 6306 }); 6307 } 6308 }); 6309 6310 // Tell the feed to do the work. 6311 this.props.dispatch(actionCreators.OnlyToMain({ 6312 type: actionTypes.UPDATE_PINNED_SEARCH_SHORTCUTS, 6313 data: { 6314 addedShortcuts: pinQueue, 6315 deletedShortcuts: unpinQueue 6316 } 6317 })); 6318 6319 // Send the Telemetry pings. 6320 pinQueue.forEach(shortcut => { 6321 this.props.dispatch(actionCreators.UserEvent({ 6322 source: TOP_SITES_SOURCE, 6323 event: "SEARCH_EDIT_ADD", 6324 value: { 6325 search_vendor: shortcut.searchVendor 6326 } 6327 })); 6328 }); 6329 unpinQueue.forEach(shortcut => { 6330 this.props.dispatch(actionCreators.UserEvent({ 6331 source: TOP_SITES_SOURCE, 6332 event: "SEARCH_EDIT_DELETE", 6333 value: { 6334 search_vendor: shortcut.searchVendor 6335 } 6336 })); 6337 }); 6338 this.props.onClose(); 6339 } 6340 _searchTopSite(shortcut) { 6341 return { 6342 url: shortcut.url, 6343 searchTopSite: true, 6344 label: shortcut.keyword, 6345 searchVendor: shortcut.shortURL 6346 }; 6347 } 6348 render() { 6349 return /*#__PURE__*/external_React_default().createElement("form", { 6350 className: "topsite-form" 6351 }, /*#__PURE__*/external_React_default().createElement("div", { 6352 className: "search-shortcuts-container" 6353 }, /*#__PURE__*/external_React_default().createElement("h3", { 6354 className: "section-title grey-title", 6355 "data-l10n-id": "newtab-topsites-add-search-engine-header" 6356 }), /*#__PURE__*/external_React_default().createElement("div", null, this.state.shortcuts.map(shortcut => /*#__PURE__*/external_React_default().createElement(SelectableSearchShortcut, { 6357 key: shortcut.keyword, 6358 shortcut: shortcut, 6359 selected: shortcut.isSelected, 6360 onChange: this.handleChange 6361 })))), /*#__PURE__*/external_React_default().createElement("section", { 6362 className: "actions" 6363 }, /*#__PURE__*/external_React_default().createElement("button", { 6364 className: "cancel", 6365 type: "button", 6366 onClick: this.onCancelButtonClick, 6367 "data-l10n-id": "newtab-topsites-cancel-button" 6368 }), /*#__PURE__*/external_React_default().createElement("button", { 6369 className: "done", 6370 type: "submit", 6371 onClick: this.onSaveButtonClick, 6372 "data-l10n-id": "newtab-topsites-save-button" 6373 }))); 6374 } 6375 } 6376 ;// CONCATENATED MODULE: ../../modules/Dedupe.sys.mjs 6377 /* This Source Code Form is subject to the terms of the Mozilla Public 6378 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 6379 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 6380 6381 class Dedupe { 6382 constructor(createKey) { 6383 this.createKey = createKey || this.defaultCreateKey; 6384 } 6385 6386 defaultCreateKey(item) { 6387 return item; 6388 } 6389 6390 /** 6391 * Dedupe any number of grouped elements favoring those from earlier groups. 6392 * 6393 * @param {Array} groups Contains an arbitrary number of arrays of elements. 6394 * @returns {Array} A matching array of each provided group deduped. 6395 */ 6396 group(...groups) { 6397 const globalKeys = new Set(); 6398 const result = []; 6399 for (const values of groups) { 6400 const valueMap = new Map(); 6401 for (const value of values) { 6402 const key = this.createKey(value); 6403 if (!globalKeys.has(key) && !valueMap.has(key)) { 6404 valueMap.set(key, value); 6405 } 6406 } 6407 result.push(valueMap); 6408 valueMap.forEach((value, key) => globalKeys.add(key)); 6409 } 6410 return result.map(m => Array.from(m.values())); 6411 } 6412 } 6413 6414 ;// CONCATENATED MODULE: ../../components/topsites/constants.mjs 6415 /* This Source Code Form is subject to the terms of the Mozilla Public 6416 * License, v. 2.0. If a copy of the MPL was not distributed with this 6417 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6418 6419 const TOP_SITES_DEFAULT_ROWS = 1; 6420 const TOP_SITES_MAX_SITES_PER_ROW = 8; 6421 6422 ;// CONCATENATED MODULE: ./common/Reducers.sys.mjs 6423 /* This Source Code Form is subject to the terms of the Mozilla Public 6424 * License, v. 2.0. If a copy of the MPL was not distributed with this 6425 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6426 6427 6428 6429 6430 6431 6432 const dedupe = new Dedupe(site => site && site.url); 6433 6434 const INITIAL_STATE = { 6435 App: { 6436 // Have we received real data from the app yet? 6437 initialized: false, 6438 locale: "", 6439 isForStartupCache: { 6440 App: false, 6441 TopSites: false, 6442 DiscoveryStream: false, 6443 Weather: false, 6444 Wallpaper: false, 6445 }, 6446 customizeMenuVisible: false, 6447 }, 6448 Ads: { 6449 initialized: false, 6450 lastUpdated: null, 6451 tiles: {}, 6452 spocs: {}, 6453 spocPlacements: {}, 6454 }, 6455 TopSites: { 6456 // Have we received real data from history yet? 6457 initialized: false, 6458 // The history (and possibly default) links 6459 rows: [], 6460 // Used in content only to dispatch action to TopSiteForm. 6461 editForm: null, 6462 // Used in content only to open the SearchShortcutsForm modal. 6463 showSearchShortcutsForm: false, 6464 // The list of available search shortcuts. 6465 searchShortcuts: [], 6466 // The "Share-of-Voice" allocations generated by TopSitesFeed 6467 sov: { 6468 ready: false, 6469 positions: [ 6470 // {position: 0, assignedPartner: "amp"}, 6471 // {position: 1, assignedPartner: "moz-sales"}, 6472 ], 6473 }, 6474 }, 6475 Prefs: { 6476 initialized: false, 6477 values: { featureConfig: {} }, 6478 }, 6479 Dialog: { 6480 visible: false, 6481 data: {}, 6482 }, 6483 Sections: [], 6484 Pocket: { 6485 pocketCta: {}, 6486 waitingForSpoc: true, 6487 }, 6488 // This is the new pocket configurable layout state. 6489 DiscoveryStream: { 6490 // This is a JSON-parsed copy of the discoverystream.config pref value. 6491 config: { enabled: false }, 6492 layout: [], 6493 topicsLoading: false, 6494 feeds: { 6495 data: { 6496 // "https://foo.com/feed1": {lastUpdated: 123, data: [], personalized: false} 6497 }, 6498 loaded: false, 6499 }, 6500 // Used to show impressions in newtab devtools. 6501 impressions: { 6502 feed: {}, 6503 }, 6504 // Used to show blocks in newtab devtools. 6505 blocks: {}, 6506 spocs: { 6507 spocs_endpoint: "", 6508 lastUpdated: null, 6509 cacheUpdateTime: null, 6510 onDemand: { 6511 enabled: false, 6512 loaded: false, 6513 }, 6514 data: { 6515 // "spocs": {title: "", context: "", items: [], personalized: false}, 6516 // "placement1": {title: "", context: "", items: [], personalized: false}, 6517 }, 6518 loaded: false, 6519 frequency_caps: [], 6520 blocked: [], 6521 placements: [], 6522 }, 6523 experimentData: { 6524 utmSource: "pocket-newtab", 6525 utmCampaign: undefined, 6526 utmContent: undefined, 6527 }, 6528 showTopicSelection: false, 6529 report: { 6530 visible: false, 6531 data: {}, 6532 }, 6533 sectionPersonalization: {}, 6534 }, 6535 // Messages received from ASRouter to render in newtab 6536 Messages: { 6537 // messages received from ASRouter are initially visible 6538 isVisible: true, 6539 // portID for that tab that was sent the message 6540 portID: "", 6541 // READONLY Message data received from ASRouter 6542 messageData: {}, 6543 }, 6544 Notifications: { 6545 showNotifications: false, 6546 toastCounter: 0, 6547 toastId: "", 6548 // This queue is reset each time SHOW_TOAST_MESSAGE is ran. 6549 // For can be a queue in the future, but for now is one item 6550 toastQueue: [], 6551 }, 6552 Personalization: { 6553 lastUpdated: null, 6554 initialized: false, 6555 }, 6556 InferredPersonalization: { 6557 initialized: false, 6558 lastUpdated: null, 6559 inferredInterests: {}, 6560 coarseInferredInterests: {}, 6561 coarsePrivateInferredInterests: {}, 6562 }, 6563 Search: { 6564 // When search hand-off is enabled, we render a big button that is styled to 6565 // look like a search textbox. If the button is clicked, we style 6566 // the button as if it was a focused search box and show a fake cursor but 6567 // really focus the awesomebar without the focus styles ("hidden focus"). 6568 fakeFocus: false, 6569 // Hide the search box after handing off to AwesomeBar and user starts typing. 6570 hide: false, 6571 }, 6572 Wallpapers: { 6573 wallpaperList: [], 6574 highlightSeenCounter: 0, 6575 categories: [], 6576 uploadedWallpaper: "", 6577 }, 6578 Weather: { 6579 initialized: false, 6580 lastUpdated: null, 6581 query: "", 6582 suggestions: [], 6583 locationData: { 6584 city: "", 6585 adminArea: "", 6586 country: "", 6587 }, 6588 // Display search input in Weather widget 6589 searchActive: false, 6590 locationSearchString: "", 6591 suggestedLocations: [], 6592 }, 6593 // Widgets 6594 ListsWidget: { 6595 // value pointing to last selectled list 6596 selected: "taskList", 6597 // Default state of an empty task list 6598 lists: { 6599 taskList: { 6600 label: "", 6601 tasks: [], 6602 completed: [], 6603 }, 6604 }, 6605 }, 6606 TimerWidget: { 6607 // The timer will have 2 types of states, focus and break. 6608 // Focus will the default state 6609 timerType: "focus", 6610 focus: { 6611 // Timer duration set by user; 25 mins by default 6612 duration: 25 * 60, 6613 // Initial duration - also set by the user; does not update until timer ends or user resets timer 6614 initialDuration: 25 * 60, 6615 // the Date.now() value when a user starts/resumes a timer 6616 startTime: null, 6617 // Boolean indicating if timer is currently running 6618 isRunning: false, 6619 }, 6620 break: { 6621 duration: 5 * 60, 6622 initialDuration: 5 * 60, 6623 startTime: null, 6624 isRunning: false, 6625 }, 6626 }, 6627 ExternalComponents: { 6628 components: [], 6629 }, 6630 }; 6631 6632 function App(prevState = INITIAL_STATE.App, action) { 6633 switch (action.type) { 6634 case actionTypes.INIT: 6635 return Object.assign({}, prevState, action.data || {}, { 6636 initialized: true, 6637 }); 6638 case actionTypes.TOP_SITES_UPDATED: 6639 // Toggle `isForStartupCache.TopSites` when receiving the `TOP_SITES_UPDATE` action 6640 // so that sponsored tiles can be rendered as usual. See Bug 1826360. 6641 return { 6642 ...prevState, 6643 isForStartupCache: { ...prevState.isForStartupCache, TopSites: false }, 6644 }; 6645 case actionTypes.DISCOVERY_STREAM_SPOCS_UPDATE: 6646 // Toggle `isForStartupCache.DiscoveryStream` when receiving the `DISCOVERY_STREAM_SPOCS_UPDATE` action 6647 // so that spoc cards can be rendered as usual. 6648 return { 6649 ...prevState, 6650 isForStartupCache: { 6651 ...prevState.isForStartupCache, 6652 DiscoveryStream: false, 6653 }, 6654 }; 6655 case actionTypes.WEATHER_UPDATE: 6656 // Toggle `isForStartupCache.Weather` when receiving the `WEATHER_UPDATE` action 6657 // so that weather can be rendered as usual. 6658 return { 6659 ...prevState, 6660 isForStartupCache: { ...prevState.isForStartupCache, Weather: false }, 6661 }; 6662 case actionTypes.WALLPAPERS_CUSTOM_SET: 6663 // Toggle `isForStartupCache.Wallpaper` when receiving the `WALLPAPERS_CUSTOM_SET` action 6664 // so that custom wallpaper can be rendered as usual. 6665 return { 6666 ...prevState, 6667 isForStartupCache: { ...prevState.isForStartupCache, Wallpaper: false }, 6668 }; 6669 case actionTypes.SHOW_PERSONALIZE: 6670 return Object.assign({}, prevState, { 6671 customizeMenuVisible: true, 6672 }); 6673 case actionTypes.HIDE_PERSONALIZE: 6674 return Object.assign({}, prevState, { 6675 customizeMenuVisible: false, 6676 }); 6677 default: 6678 return prevState; 6679 } 6680 } 6681 6682 function TopSites(prevState = INITIAL_STATE.TopSites, action) { 6683 let hasMatch; 6684 let newRows; 6685 switch (action.type) { 6686 case actionTypes.TOP_SITES_UPDATED: 6687 if (!action.data || !action.data.links) { 6688 return prevState; 6689 } 6690 return Object.assign( 6691 {}, 6692 prevState, 6693 { initialized: true, rows: action.data.links }, 6694 action.data.pref ? { pref: action.data.pref } : {} 6695 ); 6696 case actionTypes.TOP_SITES_PREFS_UPDATED: 6697 return Object.assign({}, prevState, { pref: action.data.pref }); 6698 case actionTypes.TOP_SITES_EDIT: 6699 return Object.assign({}, prevState, { 6700 editForm: { 6701 index: action.data.index, 6702 previewResponse: null, 6703 }, 6704 }); 6705 case actionTypes.TOP_SITES_CANCEL_EDIT: 6706 return Object.assign({}, prevState, { editForm: null }); 6707 case actionTypes.TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL: 6708 return Object.assign({}, prevState, { showSearchShortcutsForm: true }); 6709 case actionTypes.TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL: 6710 return Object.assign({}, prevState, { showSearchShortcutsForm: false }); 6711 case actionTypes.PREVIEW_RESPONSE: 6712 if ( 6713 !prevState.editForm || 6714 action.data.url !== prevState.editForm.previewUrl 6715 ) { 6716 return prevState; 6717 } 6718 return Object.assign({}, prevState, { 6719 editForm: { 6720 index: prevState.editForm.index, 6721 previewResponse: action.data.preview, 6722 previewUrl: action.data.url, 6723 }, 6724 }); 6725 case actionTypes.PREVIEW_REQUEST: 6726 if (!prevState.editForm) { 6727 return prevState; 6728 } 6729 return Object.assign({}, prevState, { 6730 editForm: { 6731 index: prevState.editForm.index, 6732 previewResponse: null, 6733 previewUrl: action.data.url, 6734 }, 6735 }); 6736 case actionTypes.PREVIEW_REQUEST_CANCEL: 6737 if (!prevState.editForm) { 6738 return prevState; 6739 } 6740 return Object.assign({}, prevState, { 6741 editForm: { 6742 index: prevState.editForm.index, 6743 previewResponse: null, 6744 }, 6745 }); 6746 case actionTypes.SCREENSHOT_UPDATED: 6747 newRows = prevState.rows.map(row => { 6748 if (row && row.url === action.data.url) { 6749 hasMatch = true; 6750 return Object.assign({}, row, { screenshot: action.data.screenshot }); 6751 } 6752 return row; 6753 }); 6754 return hasMatch 6755 ? Object.assign({}, prevState, { rows: newRows }) 6756 : prevState; 6757 case actionTypes.PLACES_BOOKMARK_ADDED: 6758 if (!action.data) { 6759 return prevState; 6760 } 6761 newRows = prevState.rows.map(site => { 6762 if (site && site.url === action.data.url) { 6763 const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data; 6764 return Object.assign({}, site, { 6765 bookmarkGuid, 6766 bookmarkTitle, 6767 bookmarkDateCreated: dateAdded, 6768 }); 6769 } 6770 return site; 6771 }); 6772 return Object.assign({}, prevState, { rows: newRows }); 6773 case actionTypes.PLACES_BOOKMARKS_REMOVED: 6774 if (!action.data) { 6775 return prevState; 6776 } 6777 newRows = prevState.rows.map(site => { 6778 if (site && action.data.urls.includes(site.url)) { 6779 const newSite = Object.assign({}, site); 6780 delete newSite.bookmarkGuid; 6781 delete newSite.bookmarkTitle; 6782 delete newSite.bookmarkDateCreated; 6783 return newSite; 6784 } 6785 return site; 6786 }); 6787 return Object.assign({}, prevState, { rows: newRows }); 6788 case actionTypes.PLACES_LINKS_DELETED: 6789 if (!action.data) { 6790 return prevState; 6791 } 6792 newRows = prevState.rows.filter( 6793 site => !action.data.urls.includes(site.url) 6794 ); 6795 return Object.assign({}, prevState, { rows: newRows }); 6796 case actionTypes.UPDATE_SEARCH_SHORTCUTS: 6797 return { ...prevState, searchShortcuts: action.data.searchShortcuts }; 6798 case actionTypes.SOV_UPDATED: { 6799 const sov = { 6800 ready: action.data.ready, 6801 positions: action.data.positions, 6802 }; 6803 return { ...prevState, sov }; 6804 } 6805 default: 6806 return prevState; 6807 } 6808 } 6809 6810 function Dialog(prevState = INITIAL_STATE.Dialog, action) { 6811 switch (action.type) { 6812 case actionTypes.DIALOG_OPEN: 6813 return Object.assign({}, prevState, { visible: true, data: action.data }); 6814 case actionTypes.DIALOG_CANCEL: 6815 return Object.assign({}, prevState, { visible: false }); 6816 case actionTypes.DIALOG_CLOSE: 6817 // Reset and hide the confirmation dialog once the action is complete. 6818 return Object.assign({}, INITIAL_STATE.Dialog); 6819 default: 6820 return prevState; 6821 } 6822 } 6823 6824 function Prefs(prevState = INITIAL_STATE.Prefs, action) { 6825 let newValues; 6826 switch (action.type) { 6827 case actionTypes.PREFS_INITIAL_VALUES: 6828 return Object.assign({}, prevState, { 6829 initialized: true, 6830 values: action.data, 6831 }); 6832 case actionTypes.PREF_CHANGED: 6833 newValues = Object.assign({}, prevState.values); 6834 newValues[action.data.name] = action.data.value; 6835 return Object.assign({}, prevState, { values: newValues }); 6836 default: 6837 return prevState; 6838 } 6839 } 6840 6841 function Sections(prevState = INITIAL_STATE.Sections, action) { 6842 let hasMatch; 6843 let newState; 6844 switch (action.type) { 6845 case actionTypes.SECTION_DEREGISTER: 6846 return prevState.filter(section => section.id !== action.data); 6847 case actionTypes.SECTION_REGISTER: 6848 // If section exists in prevState, update it 6849 newState = prevState.map(section => { 6850 if (section && section.id === action.data.id) { 6851 hasMatch = true; 6852 return Object.assign({}, section, action.data); 6853 } 6854 return section; 6855 }); 6856 // Otherwise, append it 6857 if (!hasMatch) { 6858 const initialized = !!(action.data.rows && !!action.data.rows.length); 6859 const section = Object.assign( 6860 { title: "", rows: [], enabled: false }, 6861 action.data, 6862 { initialized } 6863 ); 6864 newState.push(section); 6865 } 6866 return newState; 6867 case actionTypes.SECTION_UPDATE: 6868 newState = prevState.map(section => { 6869 if (section && section.id === action.data.id) { 6870 // If the action is updating rows, we should consider initialized to be true. 6871 // This can be overridden if initialized is defined in the action.data 6872 const initialized = action.data.rows ? { initialized: true } : {}; 6873 6874 // Make sure pinned cards stay at their current position when rows are updated. 6875 // Disabling a section (SECTION_UPDATE with empty rows) does not retain pinned cards. 6876 if ( 6877 action.data.rows && 6878 !!action.data.rows.length && 6879 section.rows.find(card => card.pinned) 6880 ) { 6881 const rows = Array.from(action.data.rows); 6882 section.rows.forEach((card, index) => { 6883 if (card.pinned) { 6884 // Only add it if it's not already there. 6885 if (rows[index].guid !== card.guid) { 6886 rows.splice(index, 0, card); 6887 } 6888 } 6889 }); 6890 return Object.assign( 6891 {}, 6892 section, 6893 initialized, 6894 Object.assign({}, action.data, { rows }) 6895 ); 6896 } 6897 6898 return Object.assign({}, section, initialized, action.data); 6899 } 6900 return section; 6901 }); 6902 6903 if (!action.data.dedupeConfigurations) { 6904 return newState; 6905 } 6906 6907 action.data.dedupeConfigurations.forEach(dedupeConf => { 6908 newState = newState.map(section => { 6909 if (section.id === dedupeConf.id) { 6910 const dedupedRows = dedupeConf.dedupeFrom.reduce( 6911 (rows, dedupeSectionId) => { 6912 const dedupeSection = newState.find( 6913 s => s.id === dedupeSectionId 6914 ); 6915 const [, newRows] = dedupe.group(dedupeSection.rows, rows); 6916 return newRows; 6917 }, 6918 section.rows 6919 ); 6920 6921 return Object.assign({}, section, { rows: dedupedRows }); 6922 } 6923 6924 return section; 6925 }); 6926 }); 6927 6928 return newState; 6929 case actionTypes.SECTION_UPDATE_CARD: 6930 return prevState.map(section => { 6931 if (section && section.id === action.data.id && section.rows) { 6932 const newRows = section.rows.map(card => { 6933 if (card.url === action.data.url) { 6934 return Object.assign({}, card, action.data.options); 6935 } 6936 return card; 6937 }); 6938 return Object.assign({}, section, { rows: newRows }); 6939 } 6940 return section; 6941 }); 6942 case actionTypes.PLACES_BOOKMARK_ADDED: 6943 if (!action.data) { 6944 return prevState; 6945 } 6946 return prevState.map(section => 6947 Object.assign({}, section, { 6948 rows: section.rows.map(item => { 6949 // find the item within the rows that is attempted to be bookmarked 6950 if (item.url === action.data.url) { 6951 const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data; 6952 return Object.assign({}, item, { 6953 bookmarkGuid, 6954 bookmarkTitle, 6955 bookmarkDateCreated: dateAdded, 6956 type: "bookmark", 6957 }); 6958 } 6959 return item; 6960 }), 6961 }) 6962 ); 6963 case actionTypes.PLACES_BOOKMARKS_REMOVED: 6964 if (!action.data) { 6965 return prevState; 6966 } 6967 return prevState.map(section => 6968 Object.assign({}, section, { 6969 rows: section.rows.map(item => { 6970 // find the bookmark within the rows that is attempted to be removed 6971 if (action.data.urls.includes(item.url)) { 6972 const newSite = Object.assign({}, item); 6973 delete newSite.bookmarkGuid; 6974 delete newSite.bookmarkTitle; 6975 delete newSite.bookmarkDateCreated; 6976 if (!newSite.type || newSite.type === "bookmark") { 6977 newSite.type = "history"; 6978 } 6979 return newSite; 6980 } 6981 return item; 6982 }), 6983 }) 6984 ); 6985 case actionTypes.PLACES_LINKS_DELETED: 6986 if (!action.data) { 6987 return prevState; 6988 } 6989 return prevState.map(section => 6990 Object.assign({}, section, { 6991 rows: section.rows.filter( 6992 site => !action.data.urls.includes(site.url) 6993 ), 6994 }) 6995 ); 6996 case actionTypes.PLACES_LINK_BLOCKED: 6997 if (!action.data) { 6998 return prevState; 6999 } 7000 return prevState.map(section => 7001 Object.assign({}, section, { 7002 rows: section.rows.filter(site => site.url !== action.data.url), 7003 }) 7004 ); 7005 default: 7006 return prevState; 7007 } 7008 } 7009 7010 function Messages(prevState = INITIAL_STATE.Messages, action) { 7011 switch (action.type) { 7012 case actionTypes.MESSAGE_SET: 7013 if (prevState.messageData.messageType) { 7014 return prevState; 7015 } 7016 return { 7017 ...prevState, 7018 messageData: action.data.message, 7019 portID: action.data.portID || "", 7020 }; 7021 case actionTypes.MESSAGE_TOGGLE_VISIBILITY: 7022 return { ...prevState, isVisible: action.data }; 7023 default: 7024 return prevState; 7025 } 7026 } 7027 7028 function Pocket(prevState = INITIAL_STATE.Pocket, action) { 7029 switch (action.type) { 7030 case actionTypes.POCKET_WAITING_FOR_SPOC: 7031 return { ...prevState, waitingForSpoc: action.data }; 7032 case actionTypes.POCKET_CTA: 7033 return { 7034 ...prevState, 7035 pocketCta: { 7036 ctaButton: action.data.cta_button, 7037 ctaText: action.data.cta_text, 7038 ctaUrl: action.data.cta_url, 7039 useCta: action.data.use_cta, 7040 }, 7041 }; 7042 default: 7043 return prevState; 7044 } 7045 } 7046 7047 function Reducers_sys_Personalization(prevState = INITIAL_STATE.Personalization, action) { 7048 switch (action.type) { 7049 case actionTypes.DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED: 7050 return { 7051 ...prevState, 7052 lastUpdated: action.data.lastUpdated, 7053 }; 7054 case actionTypes.DISCOVERY_STREAM_PERSONALIZATION_INIT: 7055 return { 7056 ...prevState, 7057 initialized: true, 7058 }; 7059 case actionTypes.DISCOVERY_STREAM_PERSONALIZATION_RESET: 7060 return { ...INITIAL_STATE.Personalization }; 7061 default: 7062 return prevState; 7063 } 7064 } 7065 7066 function InferredPersonalization( 7067 prevState = INITIAL_STATE.InferredPersonalization, 7068 action 7069 ) { 7070 switch (action.type) { 7071 case actionTypes.INFERRED_PERSONALIZATION_UPDATE: 7072 return { 7073 ...prevState, 7074 initialized: true, 7075 inferredInterests: action.data.inferredInterests, 7076 coarseInferredInterests: action.data.coarseInferredInterests, 7077 coarsePrivateInferredInterests: 7078 action.data.coarsePrivateInferredInterests, 7079 lastUpdated: action.data.lastUpdated, 7080 }; 7081 case actionTypes.INFERRED_PERSONALIZATION_RESET: 7082 return { ...INITIAL_STATE.InferredPersonalization }; 7083 default: 7084 return prevState; 7085 } 7086 } 7087 7088 // eslint-disable-next-line complexity 7089 function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) { 7090 // Return if action data is empty, or spocs or feeds data is not loaded 7091 const isNotReady = () => 7092 !action.data || !prevState.spocs.loaded || !prevState.feeds.loaded; 7093 7094 const handlePlacements = handleSites => { 7095 const { data, placements } = prevState.spocs; 7096 const result = {}; 7097 7098 const forPlacement = placement => { 7099 const placementSpocs = data[placement.name]; 7100 7101 if ( 7102 !placementSpocs || 7103 !placementSpocs.items || 7104 !placementSpocs.items.length 7105 ) { 7106 return; 7107 } 7108 7109 result[placement.name] = { 7110 ...placementSpocs, 7111 items: handleSites(placementSpocs.items), 7112 }; 7113 }; 7114 7115 if (!placements || !placements.length) { 7116 [{ name: "spocs" }].forEach(forPlacement); 7117 } else { 7118 placements.forEach(forPlacement); 7119 } 7120 return result; 7121 }; 7122 7123 const nextState = handleSites => ({ 7124 ...prevState, 7125 spocs: { 7126 ...prevState.spocs, 7127 data: handlePlacements(handleSites), 7128 }, 7129 feeds: { 7130 ...prevState.feeds, 7131 data: Object.keys(prevState.feeds.data).reduce( 7132 (accumulator, feed_url) => { 7133 accumulator[feed_url] = { 7134 data: { 7135 ...prevState.feeds.data[feed_url].data, 7136 recommendations: handleSites( 7137 prevState.feeds.data[feed_url].data.recommendations 7138 ), 7139 }, 7140 }; 7141 return accumulator; 7142 }, 7143 {} 7144 ), 7145 }, 7146 }); 7147 7148 switch (action.type) { 7149 case actionTypes.DISCOVERY_STREAM_CONFIG_CHANGE: 7150 // Fall through to a separate action is so it doesn't trigger a listener update on init 7151 case actionTypes.DISCOVERY_STREAM_CONFIG_SETUP: 7152 return { ...prevState, config: action.data || {} }; 7153 case actionTypes.DISCOVERY_STREAM_EXPERIMENT_DATA: 7154 return { ...prevState, experimentData: action.data || {} }; 7155 case actionTypes.DISCOVERY_STREAM_LAYOUT_UPDATE: 7156 return { 7157 ...prevState, 7158 layout: action.data.layout || [], 7159 }; 7160 case actionTypes.DISCOVERY_STREAM_TOPICS_LOADING: 7161 return { 7162 ...prevState, 7163 topicsLoading: action.data, 7164 }; 7165 case actionTypes.DISCOVERY_STREAM_PREFS_SETUP: 7166 return { 7167 ...prevState, 7168 hideDescriptions: action.data.hideDescriptions, 7169 compactImages: action.data.compactImages, 7170 imageGradient: action.data.imageGradient, 7171 newSponsoredLabel: action.data.newSponsoredLabel, 7172 titleLines: action.data.titleLines, 7173 descLines: action.data.descLines, 7174 readTime: action.data.readTime, 7175 }; 7176 case actionTypes.SHOW_PRIVACY_INFO: 7177 return { 7178 ...prevState, 7179 }; 7180 case actionTypes.DISCOVERY_STREAM_LAYOUT_RESET: 7181 return { ...INITIAL_STATE.DiscoveryStream, config: prevState.config }; 7182 case actionTypes.DISCOVERY_STREAM_FEEDS_UPDATE: 7183 return { 7184 ...prevState, 7185 feeds: { 7186 ...prevState.feeds, 7187 loaded: true, 7188 }, 7189 }; 7190 case actionTypes.DISCOVERY_STREAM_FEED_UPDATE: { 7191 const newData = {}; 7192 newData[action.data.url] = action.data.feed; 7193 return { 7194 ...prevState, 7195 feeds: { 7196 ...prevState.feeds, 7197 data: { 7198 ...prevState.feeds.data, 7199 ...newData, 7200 }, 7201 }, 7202 }; 7203 } 7204 case actionTypes.DISCOVERY_STREAM_DEV_IMPRESSIONS: 7205 return { 7206 ...prevState, 7207 impressions: { 7208 ...prevState.impressions, 7209 feed: action.data, 7210 }, 7211 }; 7212 case actionTypes.DISCOVERY_STREAM_DEV_BLOCKS: 7213 return { 7214 ...prevState, 7215 blocks: action.data, 7216 }; 7217 case actionTypes.DISCOVERY_STREAM_SPOCS_CAPS: 7218 return { 7219 ...prevState, 7220 spocs: { 7221 ...prevState.spocs, 7222 frequency_caps: [...prevState.spocs.frequency_caps, ...action.data], 7223 }, 7224 }; 7225 case actionTypes.DISCOVERY_STREAM_SPOCS_ENDPOINT: 7226 return { 7227 ...prevState, 7228 spocs: { 7229 ...INITIAL_STATE.DiscoveryStream.spocs, 7230 spocs_endpoint: 7231 action.data.url || 7232 INITIAL_STATE.DiscoveryStream.spocs.spocs_endpoint, 7233 }, 7234 }; 7235 case actionTypes.DISCOVERY_STREAM_SPOCS_PLACEMENTS: 7236 return { 7237 ...prevState, 7238 spocs: { 7239 ...prevState.spocs, 7240 placements: 7241 action.data.placements || 7242 INITIAL_STATE.DiscoveryStream.spocs.placements, 7243 }, 7244 }; 7245 case actionTypes.DISCOVERY_STREAM_SPOCS_UPDATE: 7246 if (action.data) { 7247 // If spocs have been loaded on this tab, we can ignore future updates. 7248 // This should never be true on the main store, only content pages. 7249 // We check agasint onDemand just to be safe. It generally shouldn't be needed. 7250 if (prevState.spocs?.onDemand?.loaded) { 7251 return prevState; 7252 } 7253 return { 7254 ...prevState, 7255 spocs: { 7256 ...prevState.spocs, 7257 lastUpdated: action.data.lastUpdated, 7258 data: action.data.spocs, 7259 cacheUpdateTime: action.data.spocsCacheUpdateTime, 7260 onDemand: { 7261 enabled: action.data.spocsOnDemand, 7262 loaded: false, 7263 }, 7264 loaded: true, 7265 }, 7266 }; 7267 } 7268 return prevState; 7269 case actionTypes.DISCOVERY_STREAM_SPOCS_ONDEMAND_LOAD: 7270 return { 7271 ...prevState, 7272 spocs: { 7273 ...prevState.spocs, 7274 onDemand: { 7275 ...prevState.spocs.onDemand, 7276 loaded: true, 7277 }, 7278 }, 7279 }; 7280 case actionTypes.DISCOVERY_STREAM_SPOCS_ONDEMAND_RESET: 7281 if (action.data) { 7282 return { 7283 ...prevState, 7284 spocs: { 7285 ...prevState.spocs, 7286 cacheUpdateTime: action.data.spocsCacheUpdateTime, 7287 onDemand: { 7288 ...prevState.spocs.onDemand, 7289 enabled: action.data.spocsOnDemand, 7290 }, 7291 }, 7292 }; 7293 } 7294 return prevState; 7295 case actionTypes.DISCOVERY_STREAM_SPOC_BLOCKED: 7296 return { 7297 ...prevState, 7298 spocs: { 7299 ...prevState.spocs, 7300 blocked: [...prevState.spocs.blocked, action.data.url], 7301 }, 7302 }; 7303 case actionTypes.DISCOVERY_STREAM_LINK_BLOCKED: 7304 return isNotReady() 7305 ? prevState 7306 : nextState(items => 7307 items.filter(item => item.url !== action.data.url) 7308 ); 7309 7310 case actionTypes.PLACES_BOOKMARK_ADDED: { 7311 const updateBookmarkInfo = item => { 7312 if (item.url === action.data.url) { 7313 const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data; 7314 return Object.assign({}, item, { 7315 bookmarkGuid, 7316 bookmarkTitle, 7317 bookmarkDateCreated: dateAdded, 7318 context_type: "bookmark", 7319 }); 7320 } 7321 return item; 7322 }; 7323 return isNotReady() 7324 ? prevState 7325 : nextState(items => items.map(updateBookmarkInfo)); 7326 } 7327 case actionTypes.PLACES_BOOKMARKS_REMOVED: { 7328 const removeBookmarkInfo = item => { 7329 if (action.data.urls.includes(item.url)) { 7330 const newSite = Object.assign({}, item); 7331 delete newSite.bookmarkGuid; 7332 delete newSite.bookmarkTitle; 7333 delete newSite.bookmarkDateCreated; 7334 if (!newSite.context_type || newSite.context_type === "bookmark") { 7335 newSite.context_type = "removedBookmark"; 7336 } 7337 return newSite; 7338 } 7339 return item; 7340 }; 7341 return isNotReady() 7342 ? prevState 7343 : nextState(items => items.map(removeBookmarkInfo)); 7344 } 7345 case actionTypes.TOPIC_SELECTION_SPOTLIGHT_OPEN: 7346 return { 7347 ...prevState, 7348 showTopicSelection: true, 7349 }; 7350 case actionTypes.TOPIC_SELECTION_SPOTLIGHT_CLOSE: 7351 return { 7352 ...prevState, 7353 showTopicSelection: false, 7354 }; 7355 case actionTypes.SECTION_BLOCKED: 7356 return { 7357 ...prevState, 7358 showBlockSectionConfirmation: true, 7359 sectionPersonalization: action.data, 7360 }; 7361 case actionTypes.REPORT_AD_OPEN: 7362 return { 7363 ...prevState, 7364 report: { 7365 ...prevState.report, 7366 card_type: action.data?.card_type, 7367 position: action.data?.position, 7368 placement_id: action.data?.placement_id, 7369 reporting_url: action.data?.reporting_url, 7370 url: action.data?.url, 7371 visible: true, 7372 }, 7373 }; 7374 case actionTypes.REPORT_CONTENT_OPEN: 7375 return { 7376 ...prevState, 7377 report: { 7378 ...prevState.report, 7379 card_type: action.data?.card_type, 7380 corpus_item_id: action.data?.corpus_item_id, 7381 scheduled_corpus_item_id: action.data?.scheduled_corpus_item_id, 7382 section_position: action.data?.section_position, 7383 section: action.data?.section, 7384 title: action.data?.title, 7385 topic: action.data?.topic, 7386 url: action.data?.url, 7387 visible: true, 7388 }, 7389 }; 7390 case actionTypes.REPORT_CLOSE: 7391 case actionTypes.REPORT_AD_SUBMIT: 7392 case actionTypes.REPORT_CONTENT_SUBMIT: 7393 return { 7394 ...prevState, 7395 report: { 7396 ...prevState.report, 7397 visible: false, 7398 }, 7399 }; 7400 case actionTypes.SECTION_PERSONALIZATION_UPDATE: 7401 return { ...prevState, sectionPersonalization: action.data }; 7402 default: 7403 return prevState; 7404 } 7405 } 7406 7407 function Search(prevState = INITIAL_STATE.Search, action) { 7408 switch (action.type) { 7409 case actionTypes.DISABLE_SEARCH: 7410 return Object.assign({ ...prevState, disable: true }); 7411 case actionTypes.FAKE_FOCUS_SEARCH: 7412 return Object.assign({ ...prevState, fakeFocus: true }); 7413 case actionTypes.SHOW_SEARCH: 7414 return Object.assign({ ...prevState, disable: false, fakeFocus: false }); 7415 default: 7416 return prevState; 7417 } 7418 } 7419 7420 function Wallpapers(prevState = INITIAL_STATE.Wallpapers, action) { 7421 switch (action.type) { 7422 case actionTypes.WALLPAPERS_SET: 7423 return { 7424 ...prevState, 7425 wallpaperList: action.data, 7426 }; 7427 case actionTypes.WALLPAPERS_FEATURE_HIGHLIGHT_COUNTER_INCREMENT: 7428 return { 7429 ...prevState, 7430 highlightSeenCounter: action.data, 7431 }; 7432 case actionTypes.WALLPAPERS_CATEGORY_SET: 7433 return { ...prevState, categories: action.data }; 7434 case actionTypes.WALLPAPERS_CUSTOM_SET: 7435 return { ...prevState, uploadedWallpaper: action.data }; 7436 default: 7437 return prevState; 7438 } 7439 } 7440 7441 function Notifications(prevState = INITIAL_STATE.Notifications, action) { 7442 switch (action.type) { 7443 case actionTypes.SHOW_TOAST_MESSAGE: 7444 return { 7445 ...prevState, 7446 showNotifications: action.data.showNotifications, 7447 toastCounter: prevState.toastCounter + 1, 7448 toastId: action.data.toastId, 7449 toastQueue: [action.data.toastId], 7450 }; 7451 case actionTypes.HIDE_TOAST_MESSAGE: { 7452 const { showNotifications, toastId: hiddenToastId } = action.data; 7453 const queuedToasts = [...prevState.toastQueue].filter( 7454 toastId => toastId !== hiddenToastId 7455 ); 7456 return { 7457 ...prevState, 7458 toastCounter: queuedToasts.length, 7459 toastQueue: queuedToasts, 7460 toastId: "", 7461 showNotifications, 7462 }; 7463 } 7464 default: 7465 return prevState; 7466 } 7467 } 7468 7469 function Weather(prevState = INITIAL_STATE.Weather, action) { 7470 switch (action.type) { 7471 case actionTypes.WEATHER_UPDATE: 7472 return { 7473 ...prevState, 7474 suggestions: action.data.suggestions, 7475 lastUpdated: action.data.date, 7476 locationData: action.data.locationData || prevState.locationData, 7477 initialized: true, 7478 }; 7479 case actionTypes.WEATHER_SEARCH_ACTIVE: 7480 return { ...prevState, searchActive: action.data }; 7481 case actionTypes.WEATHER_LOCATION_SEARCH_UPDATE: 7482 return { ...prevState, locationSearchString: action.data }; 7483 case actionTypes.WEATHER_LOCATION_SUGGESTIONS_UPDATE: 7484 return { ...prevState, suggestedLocations: action.data }; 7485 case actionTypes.WEATHER_LOCATION_DATA_UPDATE: 7486 return { ...prevState, locationData: action.data }; 7487 default: 7488 return prevState; 7489 } 7490 } 7491 7492 function Ads(prevState = INITIAL_STATE.Ads, action) { 7493 switch (action.type) { 7494 case actionTypes.ADS_INIT: 7495 return { 7496 ...prevState, 7497 initialized: true, 7498 }; 7499 case actionTypes.ADS_UPDATE_TILES: 7500 return { 7501 ...prevState, 7502 tiles: action.data.tiles, 7503 }; 7504 case actionTypes.ADS_UPDATE_SPOCS: 7505 return { 7506 ...prevState, 7507 spocs: action.data.spocs, 7508 spocPlacements: action.data.spocPlacements, 7509 }; 7510 case actionTypes.ADS_RESET: 7511 return { ...INITIAL_STATE.Ads }; 7512 default: 7513 return prevState; 7514 } 7515 } 7516 7517 function TimerWidget(prevState = INITIAL_STATE.TimerWidget, action) { 7518 // fallback to current timerType in state if not provided in action 7519 const timerType = action.data?.timerType || prevState.timerType; 7520 switch (action.type) { 7521 case actionTypes.WIDGETS_TIMER_SET: 7522 return { 7523 ...prevState, 7524 ...action.data, 7525 }; 7526 case actionTypes.WIDGETS_TIMER_SET_TYPE: 7527 return { 7528 ...prevState, 7529 timerType: action.data.timerType, 7530 }; 7531 case actionTypes.WIDGETS_TIMER_SET_DURATION: 7532 return { 7533 ...prevState, 7534 [timerType]: { 7535 // setting a dynamic key assignment to let us dynamically update timer type's state based on what is set 7536 duration: action.data.duration, 7537 initialDuration: action.data.duration, 7538 startTime: null, 7539 isRunning: false, 7540 }, 7541 }; 7542 case actionTypes.WIDGETS_TIMER_PLAY: 7543 return { 7544 ...prevState, 7545 [timerType]: { 7546 ...prevState[timerType], 7547 startTime: Math.floor(Date.now() / 1000), // reflected in seconds 7548 isRunning: true, 7549 }, 7550 }; 7551 case actionTypes.WIDGETS_TIMER_PAUSE: 7552 if (prevState[timerType]?.isRunning) { 7553 return { 7554 ...prevState, 7555 [timerType]: { 7556 ...prevState[timerType], 7557 duration: action.data.duration, 7558 // setting startTime to null on pause because we need to check the exact time the user presses play, 7559 // whether it's when the user starts or resumes the timer. This helps get accurate results 7560 startTime: null, 7561 isRunning: false, 7562 }, 7563 }; 7564 } 7565 return prevState; 7566 case actionTypes.WIDGETS_TIMER_RESET: 7567 return { 7568 ...prevState, 7569 [timerType]: { 7570 ...prevState[timerType], 7571 duration: action.data.duration, 7572 initialDuration: action.data.duration, 7573 startTime: null, 7574 isRunning: false, 7575 }, 7576 }; 7577 case actionTypes.WIDGETS_TIMER_END: 7578 return { 7579 ...prevState, 7580 [timerType]: { 7581 ...prevState[timerType], 7582 duration: action.data.duration, 7583 initialDuration: action.data.duration, 7584 startTime: null, 7585 isRunning: false, 7586 }, 7587 }; 7588 default: 7589 return prevState; 7590 } 7591 } 7592 7593 function ListsWidget(prevState = INITIAL_STATE.ListsWidget, action) { 7594 switch (action.type) { 7595 case actionTypes.WIDGETS_LISTS_SET: 7596 return { ...prevState, lists: action.data }; 7597 case actionTypes.WIDGETS_LISTS_SET_SELECTED: 7598 return { ...prevState, selected: action.data }; 7599 default: 7600 return prevState; 7601 } 7602 } 7603 7604 function ExternalComponents( 7605 prevState = INITIAL_STATE.ExternalComponents, 7606 action 7607 ) { 7608 switch (action.type) { 7609 case actionTypes.REFRESH_EXTERNAL_COMPONENTS: 7610 return { ...prevState, components: action.data }; 7611 default: 7612 return prevState; 7613 } 7614 } 7615 7616 const reducers = { 7617 TopSites, 7618 App, 7619 Ads, 7620 Prefs, 7621 Dialog, 7622 Sections, 7623 Messages, 7624 Notifications, 7625 Pocket, 7626 Personalization: Reducers_sys_Personalization, 7627 InferredPersonalization, 7628 DiscoveryStream, 7629 Search, 7630 TimerWidget, 7631 ListsWidget, 7632 Wallpapers, 7633 Weather, 7634 ExternalComponents, 7635 }; 7636 7637 ;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteFormInput.jsx 7638 /* This Source Code Form is subject to the terms of the Mozilla Public 7639 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 7640 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 7641 7642 7643 class TopSiteFormInput extends (external_React_default()).PureComponent { 7644 constructor(props) { 7645 super(props); 7646 this.state = { 7647 validationError: this.props.validationError 7648 }; 7649 this.onChange = this.onChange.bind(this); 7650 this.onMount = this.onMount.bind(this); 7651 this.onClearIconPress = this.onClearIconPress.bind(this); 7652 } 7653 componentWillReceiveProps(nextProps) { 7654 if (nextProps.shouldFocus && !this.props.shouldFocus) { 7655 this.input.focus(); 7656 } 7657 if (nextProps.validationError && !this.props.validationError) { 7658 this.setState({ 7659 validationError: true 7660 }); 7661 } 7662 // If the component is in an error state but the value was cleared by the parent 7663 if (this.state.validationError && !nextProps.value) { 7664 this.setState({ 7665 validationError: false 7666 }); 7667 } 7668 } 7669 onClearIconPress(event) { 7670 // If there is input in the URL or custom image URL fields, 7671 // and we hit 'enter' while tabbed over the clear icon, 7672 // we should execute the function to clear the field. 7673 if (event.key === "Enter") { 7674 this.props.onClear(); 7675 } 7676 } 7677 onChange(ev) { 7678 if (this.state.validationError) { 7679 this.setState({ 7680 validationError: false 7681 }); 7682 } 7683 this.props.onChange(ev); 7684 } 7685 onMount(input) { 7686 this.input = input; 7687 } 7688 renderLoadingOrCloseButton() { 7689 const showClearButton = this.props.value && this.props.onClear; 7690 if (this.props.loading) { 7691 return /*#__PURE__*/external_React_default().createElement("div", { 7692 className: "loading-container" 7693 }, /*#__PURE__*/external_React_default().createElement("div", { 7694 className: "loading-animation" 7695 })); 7696 } else if (showClearButton) { 7697 return /*#__PURE__*/external_React_default().createElement("button", { 7698 type: "button", 7699 className: "icon icon-clear-input icon-button-style", 7700 onClick: this.props.onClear, 7701 onKeyPress: this.onClearIconPress 7702 }); 7703 } 7704 return null; 7705 } 7706 render() { 7707 const { 7708 typeUrl 7709 } = this.props; 7710 const { 7711 validationError 7712 } = this.state; 7713 return /*#__PURE__*/external_React_default().createElement("label", null, /*#__PURE__*/external_React_default().createElement("span", { 7714 "data-l10n-id": this.props.titleId 7715 }), /*#__PURE__*/external_React_default().createElement("div", { 7716 className: `field ${typeUrl ? "url" : ""}${validationError ? " invalid" : ""}` 7717 }, /*#__PURE__*/external_React_default().createElement("input", { 7718 type: "text", 7719 value: this.props.value, 7720 ref: this.onMount, 7721 onChange: this.onChange, 7722 "data-l10n-id": this.props.placeholderId 7723 // Set focus on error if the url field is valid or when the input is first rendered and is empty 7724 // eslint-disable-next-line jsx-a11y/no-autofocus 7725 , 7726 autoFocus: this.props.autoFocusOnOpen, 7727 disabled: this.props.loading 7728 }), this.renderLoadingOrCloseButton(), validationError && /*#__PURE__*/external_React_default().createElement("aside", { 7729 className: "error-tooltip", 7730 "data-l10n-id": this.props.errorMessageId 7731 }))); 7732 } 7733 } 7734 TopSiteFormInput.defaultProps = { 7735 showClearButton: false, 7736 value: "", 7737 validationError: false 7738 }; 7739 ;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteImpressionWrapper.jsx 7740 /* This Source Code Form is subject to the terms of the Mozilla Public 7741 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 7742 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 7743 7744 7745 7746 const TopSiteImpressionWrapper_VISIBLE = "visible"; 7747 const TopSiteImpressionWrapper_VISIBILITY_CHANGE_EVENT = "visibilitychange"; 7748 7749 // Per analytical requirement, we set the minimal intersection ratio to 7750 // 0.5, and an impression is identified when the wrapped item has at least 7751 // 50% visibility. 7752 // 7753 // This constant is exported for unit test 7754 const TopSiteImpressionWrapper_INTERSECTION_RATIO = 0.5; 7755 7756 /** 7757 * Impression wrapper for a TopSite tile. 7758 * 7759 * It makses use of the Intersection Observer API to detect the visibility, 7760 * and relies on page visibility to ensure the impression is reported 7761 * only when the component is visible on the page. 7762 */ 7763 class TopSiteImpressionWrapper extends (external_React_default()).PureComponent { 7764 _dispatchImpressionStats() { 7765 const { 7766 actionType, 7767 tile 7768 } = this.props; 7769 if (!actionType) { 7770 return; 7771 } 7772 this.props.dispatch(actionCreators.OnlyToMain({ 7773 type: actionType, 7774 data: { 7775 type: "impression", 7776 ...tile 7777 } 7778 })); 7779 } 7780 setImpressionObserverOrAddListener() { 7781 const { 7782 props 7783 } = this; 7784 if (!props.dispatch) { 7785 return; 7786 } 7787 if (props.document.visibilityState === TopSiteImpressionWrapper_VISIBLE) { 7788 this.setImpressionObserver(); 7789 } else { 7790 // We should only ever send the latest impression stats ping, so remove any 7791 // older listeners. 7792 if (this._onVisibilityChange) { 7793 props.document.removeEventListener(TopSiteImpressionWrapper_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 7794 } 7795 this._onVisibilityChange = () => { 7796 if (props.document.visibilityState === TopSiteImpressionWrapper_VISIBLE) { 7797 this.setImpressionObserver(); 7798 props.document.removeEventListener(TopSiteImpressionWrapper_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 7799 } 7800 }; 7801 props.document.addEventListener(TopSiteImpressionWrapper_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 7802 } 7803 } 7804 7805 /** 7806 * Set an impression observer for the wrapped component. It makes use of 7807 * the Intersection Observer API to detect if the wrapped component is 7808 * visible with a desired ratio, and only sends impression if that's the case. 7809 * 7810 * See more details about Intersection Observer API at: 7811 * https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API 7812 */ 7813 setImpressionObserver() { 7814 const { 7815 props 7816 } = this; 7817 if (!props.tile) { 7818 return; 7819 } 7820 this._handleIntersect = entries => { 7821 if (entries.some(entry => entry.isIntersecting && entry.intersectionRatio >= TopSiteImpressionWrapper_INTERSECTION_RATIO)) { 7822 this._dispatchImpressionStats(); 7823 this.impressionObserver.unobserve(this.refs.topsite_impression_wrapper); 7824 } 7825 }; 7826 const options = { 7827 threshold: TopSiteImpressionWrapper_INTERSECTION_RATIO 7828 }; 7829 this.impressionObserver = new props.IntersectionObserver(this._handleIntersect, options); 7830 this.impressionObserver.observe(this.refs.topsite_impression_wrapper); 7831 } 7832 componentDidMount() { 7833 if (this.props.tile) { 7834 this.setImpressionObserverOrAddListener(); 7835 } 7836 } 7837 componentWillUnmount() { 7838 if (this._handleIntersect && this.impressionObserver) { 7839 this.impressionObserver.unobserve(this.refs.topsite_impression_wrapper); 7840 } 7841 if (this._onVisibilityChange) { 7842 this.props.document.removeEventListener(TopSiteImpressionWrapper_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 7843 } 7844 } 7845 render() { 7846 return /*#__PURE__*/external_React_default().createElement("div", { 7847 ref: "topsite_impression_wrapper", 7848 className: "topsite-impression-observer" 7849 }, this.props.children); 7850 } 7851 } 7852 TopSiteImpressionWrapper.defaultProps = { 7853 IntersectionObserver: globalThis.IntersectionObserver, 7854 document: globalThis.document, 7855 actionType: null, 7856 tile: null 7857 }; 7858 ;// CONCATENATED MODULE: ./content-src/components/MessageWrapper/MessageWrapper.jsx 7859 /* This Source Code Form is subject to the terms of the Mozilla Public 7860 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 7861 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 7862 7863 7864 7865 7866 7867 7868 // Note: MessageWrapper emits events via submitGleanPingForPing() in the OMC messaging-system. 7869 // If a feature is triggered outside of this flow (e.g., the Mobile Download QR Promo), 7870 // it should emit New Tab-specific Glean events independently. 7871 7872 function MessageWrapper({ 7873 children, 7874 dispatch, 7875 hiddenOverride, 7876 onDismiss 7877 }) { 7878 const message = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Messages); 7879 const [isIntersecting, setIsIntersecting] = (0,external_React_namespaceObject.useState)(false); 7880 const [tabIsVisible, setTabIsVisible] = (0,external_React_namespaceObject.useState)(() => typeof document !== "undefined" && document.visibilityState === "visible"); 7881 const [hasRun, setHasRun] = (0,external_React_namespaceObject.useState)(); 7882 const handleIntersection = (0,external_React_namespaceObject.useCallback)(() => { 7883 setIsIntersecting(true); 7884 // only send impression if messageId is defined and tab is visible 7885 if (tabIsVisible && message.messageData.id && !hasRun) { 7886 setHasRun(true); 7887 dispatch(actionCreators.AlsoToMain({ 7888 type: actionTypes.MESSAGE_IMPRESSION, 7889 data: message.messageData 7890 })); 7891 } 7892 }, [dispatch, message, tabIsVisible, hasRun]); 7893 (0,external_React_namespaceObject.useEffect)(() => { 7894 // we dont want to dispatch this action unless the current tab is open and visible 7895 if (message.isVisible && tabIsVisible) { 7896 dispatch(actionCreators.AlsoToMain({ 7897 type: actionTypes.MESSAGE_NOTIFY_VISIBILITY, 7898 data: true 7899 })); 7900 } 7901 }, [message, dispatch, tabIsVisible]); 7902 (0,external_React_namespaceObject.useEffect)(() => { 7903 const handleVisibilityChange = () => { 7904 setTabIsVisible(document.visibilityState === "visible"); 7905 }; 7906 document.addEventListener("visibilitychange", handleVisibilityChange); 7907 return () => { 7908 document.removeEventListener("visibilitychange", handleVisibilityChange); 7909 }; 7910 }, []); 7911 const ref = useIntersectionObserver(handleIntersection); 7912 const handleClose = (0,external_React_namespaceObject.useCallback)(() => { 7913 const action = { 7914 type: actionTypes.MESSAGE_TOGGLE_VISIBILITY, 7915 data: false //isVisible 7916 }; 7917 if (message.portID) { 7918 dispatch(actionCreators.OnlyToOneContent(action, message.portID)); 7919 } else { 7920 dispatch(actionCreators.AlsoToMain(action)); 7921 } 7922 dispatch(actionCreators.AlsoToMain({ 7923 type: actionTypes.MESSAGE_NOTIFY_VISIBILITY, 7924 data: false 7925 })); 7926 onDismiss?.(); 7927 }, [dispatch, message, onDismiss]); 7928 function handleDismiss() { 7929 const { 7930 id 7931 } = message.messageData; 7932 if (id) { 7933 dispatch(actionCreators.OnlyToMain({ 7934 type: actionTypes.MESSAGE_DISMISS, 7935 data: { 7936 message: message.messageData 7937 } 7938 })); 7939 } 7940 handleClose(); 7941 } 7942 function handleBlock() { 7943 const { 7944 id 7945 } = message.messageData; 7946 if (id) { 7947 dispatch(actionCreators.OnlyToMain({ 7948 type: actionTypes.MESSAGE_BLOCK, 7949 data: id 7950 })); 7951 } 7952 } 7953 function handleClick(elementId) { 7954 const { 7955 id 7956 } = message.messageData; 7957 if (id) { 7958 dispatch(actionCreators.OnlyToMain({ 7959 type: actionTypes.MESSAGE_CLICK, 7960 data: { 7961 message: message.messageData, 7962 source: elementId || "" 7963 } 7964 })); 7965 } 7966 } 7967 if (!message || !hiddenOverride && !message.isVisible) { 7968 return null; 7969 } 7970 7971 // only display the message if `isVisible` is true 7972 return /*#__PURE__*/external_React_default().createElement("div", { 7973 ref: el => { 7974 ref.current = [el]; 7975 }, 7976 className: "message-wrapper" 7977 }, /*#__PURE__*/external_React_default().cloneElement(children, { 7978 isIntersecting, 7979 handleDismiss, 7980 handleClick, 7981 handleBlock, 7982 handleClose 7983 })); 7984 } 7985 7986 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx 7987 /* This Source Code Form is subject to the terms of the Mozilla Public 7988 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 7989 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 7990 7991 7992 7993 function FeatureHighlight({ 7994 message, 7995 icon, 7996 toggle, 7997 arrowPosition = "", 7998 position = "top-left", 7999 verticalPosition = "", 8000 title, 8001 ariaLabel, 8002 feature = "FEATURE_HIGHLIGHT_DEFAULT", 8003 dispatch = () => {}, 8004 windowObj = __webpack_require__.g, 8005 openedOverride = false, 8006 showButtonIcon = true, 8007 dismissCallback = () => {}, 8008 outsideClickCallback = () => {}, 8009 modalClassName = "" 8010 }) { 8011 const [opened, setOpened] = (0,external_React_namespaceObject.useState)(openedOverride); 8012 const ref = (0,external_React_namespaceObject.useRef)(null); 8013 (0,external_React_namespaceObject.useEffect)(() => { 8014 const handleOutsideClick = e => { 8015 if (!ref?.current?.contains(e.target)) { 8016 setOpened(false); 8017 outsideClickCallback(); 8018 } 8019 }; 8020 const handleKeyDown = e => { 8021 if (e.key === "Escape") { 8022 outsideClickCallback(); 8023 } 8024 }; 8025 windowObj.document.addEventListener("click", handleOutsideClick); 8026 windowObj.document.addEventListener("keydown", handleKeyDown); 8027 return () => { 8028 windowObj.document.removeEventListener("click", handleOutsideClick); 8029 windowObj.document.removeEventListener("keydown", handleKeyDown); 8030 }; 8031 }, [windowObj, outsideClickCallback]); 8032 const onToggleClick = (0,external_React_namespaceObject.useCallback)(() => { 8033 if (!opened) { 8034 dispatch(actionCreators.DiscoveryStreamUserEvent({ 8035 event: "CLICK", 8036 source: "FEATURE_HIGHLIGHT", 8037 value: { 8038 feature 8039 } 8040 })); 8041 } 8042 setOpened(!opened); 8043 }, [dispatch, feature, opened]); 8044 const onDismissClick = (0,external_React_namespaceObject.useCallback)(() => { 8045 setOpened(false); 8046 dismissCallback(); 8047 }, [dismissCallback]); 8048 const hideButtonClass = showButtonIcon ? `` : `isHidden`; 8049 const openedClassname = opened ? `opened` : `closed`; 8050 return /*#__PURE__*/external_React_default().createElement("div", { 8051 ref: ref, 8052 className: `feature-highlight ${verticalPosition}` 8053 }, /*#__PURE__*/external_React_default().createElement("button", { 8054 title: title, 8055 "aria-haspopup": "true", 8056 "aria-label": ariaLabel, 8057 className: `toggle-button ${hideButtonClass}`, 8058 onClick: onToggleClick 8059 }, toggle), /*#__PURE__*/external_React_default().createElement("div", { 8060 className: `feature-highlight-modal ${position} ${arrowPosition} ${modalClassName} ${openedClassname}` 8061 }, /*#__PURE__*/external_React_default().createElement("div", { 8062 className: "message-icon" 8063 }, icon), /*#__PURE__*/external_React_default().createElement("p", { 8064 className: "content-wrapper" 8065 }, message), /*#__PURE__*/external_React_default().createElement("moz-button", { 8066 type: "icon ghost", 8067 size: "small", 8068 "data-l10n-id": "feature-highlight-dismiss-button", 8069 iconsrc: "chrome://global/skin/icons/close.svg", 8070 onClick: onDismissClick, 8071 onKeyDown: onDismissClick 8072 }))); 8073 } 8074 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/FeatureHighlight/ShortcutFeatureHighlight.jsx 8075 /* This Source Code Form is subject to the terms of the Mozilla Public 8076 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 8077 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 8078 8079 8080 8081 function ShortcutFeatureHighlight({ 8082 dispatch, 8083 feature, 8084 handleBlock, 8085 handleDismiss, 8086 messageData, 8087 position 8088 }) { 8089 const onDismiss = (0,external_React_namespaceObject.useCallback)(() => { 8090 handleDismiss(); 8091 handleBlock(); 8092 }, [handleDismiss, handleBlock]); 8093 return /*#__PURE__*/external_React_default().createElement("div", { 8094 className: `shortcut-feature-highlight ${messageData.content?.darkModeDismiss ? "is-inverted-dark-dismiss-button" : ""}` 8095 }, /*#__PURE__*/external_React_default().createElement(FeatureHighlight, { 8096 position: position, 8097 feature: feature, 8098 dispatch: dispatch, 8099 message: /*#__PURE__*/external_React_default().createElement("div", { 8100 className: "shortcut-feature-highlight-content" 8101 }, /*#__PURE__*/external_React_default().createElement("picture", { 8102 className: "follow-section-button-highlight-image" 8103 }, /*#__PURE__*/external_React_default().createElement("source", { 8104 srcSet: messageData.content?.darkModeImageURL || "chrome://newtab/content/data/content/assets/highlights/omc-newtab-shortcuts.svg", 8105 media: "(prefers-color-scheme: dark)" 8106 }), /*#__PURE__*/external_React_default().createElement("source", { 8107 srcSet: messageData.content?.imageURL || "chrome://newtab/content/data/content/assets/highlights/omc-newtab-shortcuts.svg", 8108 media: "(prefers-color-scheme: light)" 8109 }), /*#__PURE__*/external_React_default().createElement("img", { 8110 width: "320", 8111 height: "195", 8112 alt: "" 8113 })), /*#__PURE__*/external_React_default().createElement("div", { 8114 className: "shortcut-feature-highlight-copy" 8115 }, messageData.content?.cardTitle ? /*#__PURE__*/external_React_default().createElement("p", { 8116 className: "title" 8117 }, messageData.content.cardTitle) : /*#__PURE__*/external_React_default().createElement("p", { 8118 className: "title", 8119 "data-l10n-id": "newtab-shortcuts-highlight-title" 8120 }), messageData.content?.cardMessage ? /*#__PURE__*/external_React_default().createElement("p", { 8121 className: "subtitle" 8122 }, messageData.content.cardMessage) : /*#__PURE__*/external_React_default().createElement("p", { 8123 className: "subtitle", 8124 "data-l10n-id": "newtab-shortcuts-highlight-subtitle" 8125 }))), 8126 openedOverride: true, 8127 showButtonIcon: false, 8128 dismissCallback: onDismiss, 8129 outsideClickCallback: handleDismiss 8130 })); 8131 } 8132 ;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSite.jsx 8133 function TopSite_extends() { return TopSite_extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, TopSite_extends.apply(null, arguments); } 8134 /* This Source Code Form is subject to the terms of the Mozilla Public 8135 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 8136 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 8137 8138 8139 8140 8141 8142 8143 8144 8145 8146 8147 8148 8149 8150 const SPOC_TYPE = "SPOC"; 8151 const NEWTAB_SOURCE = "newtab"; 8152 8153 // For cases if we want to know if this is sponsored by either sponsored_position or type. 8154 // We have two sources for sponsored topsites, and 8155 // sponsored_position is set by one sponsored source, and type is set by another. 8156 // This is not called in all cases, sometimes we want to know if it's one source 8157 // or the other. This function is only applicable in cases where we only care if it's either. 8158 function isSponsored(link) { 8159 return link?.sponsored_position || link?.type === SPOC_TYPE; 8160 } 8161 class TopSiteLink extends (external_React_default()).PureComponent { 8162 constructor(props) { 8163 super(props); 8164 this.state = { 8165 screenshotImage: null 8166 }; 8167 this.onDragEvent = this.onDragEvent.bind(this); 8168 this.onKeyPress = this.onKeyPress.bind(this); 8169 this.shouldShowOMCHighlight = this.shouldShowOMCHighlight.bind(this); 8170 } 8171 8172 /* 8173 * Helper to determine whether the drop zone should allow a drop. We only allow 8174 * dropping top sites for now. We don't allow dropping on sponsored top sites 8175 * as their position is fixed. 8176 */ 8177 _allowDrop(e) { 8178 return (this.dragged || !isSponsored(this.props.link)) && e.dataTransfer.types.includes("text/topsite-index"); 8179 } 8180 onDragEvent(event) { 8181 switch (event.type) { 8182 case "click": 8183 // Stop any link clicks if we started any dragging 8184 if (this.dragged) { 8185 event.preventDefault(); 8186 } 8187 break; 8188 case "dragstart": 8189 event.target.blur(); 8190 if (isSponsored(this.props.link)) { 8191 event.preventDefault(); 8192 break; 8193 } 8194 this.dragged = true; 8195 event.dataTransfer.effectAllowed = "move"; 8196 event.dataTransfer.setData("text/topsite-index", this.props.index); 8197 this.props.onDragEvent(event, this.props.index, this.props.link, this.props.title); 8198 break; 8199 case "dragend": 8200 this.props.onDragEvent(event); 8201 break; 8202 case "dragenter": 8203 case "dragover": 8204 case "drop": 8205 if (this._allowDrop(event)) { 8206 event.preventDefault(); 8207 this.props.onDragEvent(event, this.props.index); 8208 } 8209 break; 8210 case "mousedown": 8211 // Block the scroll wheel from appearing for middle clicks on search top sites 8212 if (event.button === 1 && this.props.link.searchTopSite) { 8213 event.preventDefault(); 8214 } 8215 // Reset at the first mouse event of a potential drag 8216 this.dragged = false; 8217 break; 8218 } 8219 } 8220 8221 /** 8222 * Helper to obtain the next state based on nextProps and prevState. 8223 * 8224 * NOTE: Rename this method to getDerivedStateFromProps when we update React 8225 * to >= 16.3. We will need to update tests as well. We cannot rename this 8226 * method to getDerivedStateFromProps now because there is a mismatch in 8227 * the React version that we are using for both testing and production. 8228 * (i.e. react-test-render => "16.3.2", react => "16.2.0"). 8229 * 8230 * See https://github.com/airbnb/enzyme/blob/master/packages/enzyme-adapter-react-16/package.json#L43. 8231 */ 8232 static getNextStateFromProps(nextProps, prevState) { 8233 const { 8234 screenshot 8235 } = nextProps.link; 8236 const imageInState = ScreenshotUtils.isRemoteImageLocal(prevState.screenshotImage, screenshot); 8237 if (imageInState) { 8238 return null; 8239 } 8240 8241 // Since image was updated, attempt to revoke old image blob URL, if it exists. 8242 ScreenshotUtils.maybeRevokeBlobObjectURL(prevState.screenshotImage); 8243 return { 8244 screenshotImage: ScreenshotUtils.createLocalImageObject(screenshot) 8245 }; 8246 } 8247 8248 // NOTE: Remove this function when we update React to >= 16.3 since React will 8249 // call getDerivedStateFromProps automatically. We will also need to 8250 // rename getNextStateFromProps to getDerivedStateFromProps. 8251 componentWillMount() { 8252 const nextState = TopSiteLink.getNextStateFromProps(this.props, this.state); 8253 if (nextState) { 8254 this.setState(nextState); 8255 } 8256 } 8257 8258 // NOTE: Remove this function when we update React to >= 16.3 since React will 8259 // call getDerivedStateFromProps automatically. We will also need to 8260 // rename getNextStateFromProps to getDerivedStateFromProps. 8261 componentWillReceiveProps(nextProps) { 8262 const nextState = TopSiteLink.getNextStateFromProps(nextProps, this.state); 8263 if (nextState) { 8264 this.setState(nextState); 8265 } 8266 } 8267 componentWillUnmount() { 8268 ScreenshotUtils.maybeRevokeBlobObjectURL(this.state.screenshotImage); 8269 } 8270 onKeyPress(event) { 8271 // If we have tabbed to a search shortcut top site, and we click 'enter', 8272 // we should execute the onClick function. This needs to be added because 8273 // search top sites are anchor tags without an href. See bug 1483135 8274 if (event.key === "Enter" && (this.props.link.searchTopSite || this.props.isAddButton)) { 8275 this.props.onClick(event); 8276 } 8277 } 8278 8279 /* 8280 * Takes the url as a string, runs it through a simple (non-secure) hash turning it into a random number 8281 * Apply that random number to the color array. The same url will always generate the same color. 8282 */ 8283 generateColor() { 8284 let { 8285 title, 8286 colors 8287 } = this.props; 8288 if (!colors) { 8289 return ""; 8290 } 8291 let colorArray = colors.split(","); 8292 const hashStr = str => { 8293 let hash = 0; 8294 for (let i = 0; i < str.length; i++) { 8295 let charCode = str.charCodeAt(i); 8296 hash += charCode; 8297 } 8298 return hash; 8299 }; 8300 let hash = hashStr(title); 8301 let index = hash % colorArray.length; 8302 return colorArray[index]; 8303 } 8304 calculateStyle() { 8305 const { 8306 defaultStyle, 8307 link 8308 } = this.props; 8309 const { 8310 tippyTopIcon, 8311 faviconSize 8312 } = link; 8313 let imageClassName; 8314 let imageStyle; 8315 let showSmallFavicon = false; 8316 let smallFaviconStyle; 8317 let hasScreenshotImage = this.state.screenshotImage && this.state.screenshotImage.url; 8318 let selectedColor; 8319 if (defaultStyle) { 8320 // force no styles (letter fallback) even if the link has imagery 8321 selectedColor = this.generateColor(); 8322 } else if (link.searchTopSite) { 8323 imageClassName = "top-site-icon rich-icon"; 8324 imageStyle = { 8325 backgroundColor: link.backgroundColor, 8326 backgroundImage: `url(${tippyTopIcon})` 8327 }; 8328 smallFaviconStyle = { 8329 backgroundImage: `url(${tippyTopIcon})` 8330 }; 8331 } else if (link.customScreenshotURL) { 8332 // assume high quality custom screenshot and use rich icon styles and class names 8333 imageClassName = "top-site-icon rich-icon"; 8334 imageStyle = { 8335 backgroundColor: link.backgroundColor, 8336 backgroundImage: hasScreenshotImage ? `url(${this.state.screenshotImage.url})` : "" 8337 }; 8338 } else if (tippyTopIcon || link.type === SPOC_TYPE || faviconSize >= MIN_RICH_FAVICON_SIZE) { 8339 // styles and class names for top sites with rich icons 8340 imageClassName = "top-site-icon rich-icon"; 8341 imageStyle = { 8342 backgroundColor: link.backgroundColor, 8343 backgroundImage: `url(${tippyTopIcon || link.favicon})` 8344 }; 8345 } else if (faviconSize >= MIN_SMALL_FAVICON_SIZE) { 8346 showSmallFavicon = true; 8347 smallFaviconStyle = { 8348 backgroundImage: `url(${link.favicon})` 8349 }; 8350 } else { 8351 selectedColor = this.generateColor(); 8352 imageClassName = ""; 8353 } 8354 return { 8355 showSmallFavicon, 8356 smallFaviconStyle, 8357 imageStyle, 8358 imageClassName, 8359 selectedColor 8360 }; 8361 } 8362 shouldShowOMCHighlight(componentId) { 8363 const messageData = this.props.Messages?.messageData; 8364 if (!messageData || Object.keys(messageData).length === 0) { 8365 return false; 8366 } 8367 return messageData?.content?.messageType === componentId; 8368 } 8369 render() { 8370 const { 8371 children, 8372 className, 8373 isDraggable, 8374 link, 8375 onClick, 8376 title, 8377 isAddButton, 8378 visibleTopSites 8379 } = this.props; 8380 const topSiteOuterClassName = `top-site-outer${className ? ` ${className}` : ""}${link.isDragged ? " dragged" : ""}${link.searchTopSite ? " search-shortcut" : ""}`; 8381 const [letterFallback] = title; 8382 const { 8383 showSmallFavicon, 8384 smallFaviconStyle, 8385 imageStyle, 8386 imageClassName, 8387 selectedColor 8388 } = this.calculateStyle(); 8389 const addButtonLabell10n = { 8390 "data-l10n-id": "newtab-topsites-add-shortcut-label" 8391 }; 8392 const addButtonTitlel10n = { 8393 "data-l10n-id": "newtab-topsites-add-shortcut-title" 8394 }; 8395 const addPinnedTitlel10n = { 8396 "data-l10n-id": "topsite-label-pinned", 8397 "data-l10n-args": JSON.stringify({ 8398 title 8399 }) 8400 }; 8401 let draggableProps = {}; 8402 if (isDraggable) { 8403 draggableProps = { 8404 onClick: this.onDragEvent, 8405 onDragEnd: this.onDragEvent, 8406 onDragStart: this.onDragEvent, 8407 onMouseDown: this.onDragEvent 8408 }; 8409 } 8410 let impressionStats = null; 8411 if (link.type === SPOC_TYPE) { 8412 // Record impressions for Pocket tiles. 8413 impressionStats = /*#__PURE__*/external_React_default().createElement(ImpressionStats_ImpressionStats, { 8414 flightId: link.flightId, 8415 rows: [{ 8416 id: link.id, 8417 pos: link.pos, 8418 shim: link.shim && link.shim.impression, 8419 advertiser: title.toLocaleLowerCase() 8420 }], 8421 dispatch: this.props.dispatch, 8422 source: TOP_SITES_SOURCE 8423 }); 8424 } else if (isSponsored(link)) { 8425 // Record impressions for non-Pocket sponsored tiles. 8426 impressionStats = /*#__PURE__*/external_React_default().createElement(TopSiteImpressionWrapper, { 8427 actionType: actionTypes.TOP_SITES_SPONSORED_IMPRESSION_STATS, 8428 tile: { 8429 position: this.props.index, 8430 tile_id: link.sponsored_tile_id || -1, 8431 reporting_url: link.sponsored_impression_url, 8432 advertiser: title.toLocaleLowerCase(), 8433 source: NEWTAB_SOURCE, 8434 visible_topsites: visibleTopSites, 8435 frecency_boosted: link.type === "frecency-boost", 8436 attribution: link.attribution 8437 } 8438 // For testing. 8439 , 8440 IntersectionObserver: this.props.IntersectionObserver, 8441 document: this.props.document, 8442 dispatch: this.props.dispatch 8443 }); 8444 } else { 8445 // Record impressions for organic tiles. 8446 impressionStats = /*#__PURE__*/external_React_default().createElement(TopSiteImpressionWrapper, { 8447 actionType: actionTypes.TOP_SITES_ORGANIC_IMPRESSION_STATS, 8448 tile: { 8449 position: this.props.index, 8450 source: NEWTAB_SOURCE, 8451 isPinned: this.props.link.isPinned, 8452 guid: this.props.link.guid, 8453 visible_topsites: visibleTopSites, 8454 smartScores: this.props.link.scores, 8455 smartWeights: this.props.link.weights 8456 } 8457 // For testing. 8458 , 8459 IntersectionObserver: this.props.IntersectionObserver, 8460 document: this.props.document, 8461 dispatch: this.props.dispatch 8462 }); 8463 } 8464 return /*#__PURE__*/external_React_default().createElement("li", TopSite_extends({ 8465 className: topSiteOuterClassName, 8466 onDrop: this.onDragEvent, 8467 onDragOver: this.onDragEvent, 8468 onDragEnter: this.onDragEvent, 8469 onDragLeave: this.onDragEvent, 8470 ref: this.props.setRef 8471 }, draggableProps), /*#__PURE__*/external_React_default().createElement("div", { 8472 className: "top-site-inner" 8473 }, /*#__PURE__*/external_React_default().createElement("a", TopSite_extends({ 8474 className: "top-site-button", 8475 href: link.searchTopSite ? undefined : link.url, 8476 tabIndex: this.props.tabIndex, 8477 onKeyPress: this.onKeyPress, 8478 onClick: onClick, 8479 draggable: true, 8480 "data-is-sponsored-link": !!link.sponsored_tile_id, 8481 onFocus: this.props.onFocus, 8482 "aria-label": link.isPinned ? undefined : title 8483 }, isAddButton && { 8484 ...addButtonTitlel10n 8485 }, !isAddButton && { 8486 title 8487 }, link.isPinned && { 8488 ...addPinnedTitlel10n 8489 }, { 8490 "data-l10n-args": JSON.stringify({ 8491 title 8492 }) 8493 }), link.isPinned && /*#__PURE__*/external_React_default().createElement("div", { 8494 className: "icon icon-pin-small" 8495 }), /*#__PURE__*/external_React_default().createElement("div", { 8496 className: "tile", 8497 "aria-hidden": true 8498 }, /*#__PURE__*/external_React_default().createElement("div", { 8499 className: selectedColor ? "icon-wrapper letter-fallback" : "icon-wrapper", 8500 "data-fallback": letterFallback, 8501 style: selectedColor ? { 8502 backgroundColor: selectedColor 8503 } : {} 8504 }, /*#__PURE__*/external_React_default().createElement("div", { 8505 className: imageClassName, 8506 style: imageStyle 8507 }), showSmallFavicon && /*#__PURE__*/external_React_default().createElement("div", { 8508 className: "top-site-icon default-icon", 8509 "data-fallback": smallFaviconStyle ? "" : letterFallback, 8510 style: smallFaviconStyle 8511 }))), /*#__PURE__*/external_React_default().createElement("div", { 8512 className: `title${link.isPinned ? " has-icon pinned" : ""}${link.type === SPOC_TYPE || link.show_sponsored_label ? " sponsored" : ""}` 8513 }, /*#__PURE__*/external_React_default().createElement("span", TopSite_extends({ 8514 className: "title-label", 8515 dir: "auto" 8516 }, isAddButton && { 8517 ...addButtonLabell10n 8518 }), link.searchTopSite && /*#__PURE__*/external_React_default().createElement("div", { 8519 className: "top-site-icon search-topsite" 8520 }), title || /*#__PURE__*/external_React_default().createElement("br", null)), /*#__PURE__*/external_React_default().createElement("span", { 8521 className: "sponsored-label", 8522 "data-l10n-id": "newtab-topsite-sponsored" 8523 }))), isAddButton && this.shouldShowOMCHighlight("ShortcutHighlight") && /*#__PURE__*/external_React_default().createElement(MessageWrapper, { 8524 dispatch: this.props.dispatch, 8525 onClick: e => e.stopPropagation() 8526 }, /*#__PURE__*/external_React_default().createElement(ShortcutFeatureHighlight, { 8527 dispatch: this.props.dispatch, 8528 feature: "FEATURE_SHORTCUT_HIGHLIGHT", 8529 position: "inset-block-end inset-inline-start", 8530 messageData: this.props.Messages?.messageData 8531 })), children, impressionStats)); 8532 } 8533 } 8534 TopSiteLink.defaultProps = { 8535 title: "", 8536 link: {}, 8537 isDraggable: true 8538 }; 8539 class TopSite extends (external_React_default()).PureComponent { 8540 constructor(props) { 8541 super(props); 8542 this.state = { 8543 showContextMenu: false 8544 }; 8545 this.onLinkClick = this.onLinkClick.bind(this); 8546 this.onMenuUpdate = this.onMenuUpdate.bind(this); 8547 } 8548 8549 /** 8550 * Report to telemetry additional information about the item. 8551 */ 8552 _getTelemetryInfo() { 8553 const value = { 8554 icon_type: this.props.link.iconType 8555 }; 8556 // Filter out "not_pinned" type for being the default 8557 if (this.props.link.isPinned) { 8558 value.card_type = "pinned"; 8559 } 8560 if (this.props.link.searchTopSite) { 8561 // Set the card_type as "search" regardless of its pinning status 8562 value.card_type = "search"; 8563 value.search_vendor = this.props.link.hostname; 8564 } 8565 if (isSponsored(this.props.link)) { 8566 value.card_type = "spoc"; 8567 } 8568 return { 8569 value 8570 }; 8571 } 8572 userEvent(event) { 8573 this.props.dispatch(actionCreators.UserEvent(Object.assign({ 8574 event, 8575 source: TOP_SITES_SOURCE, 8576 action_position: this.props.index 8577 }, this._getTelemetryInfo()))); 8578 } 8579 onLinkClick(event) { 8580 this.userEvent("CLICK"); 8581 8582 // Specially handle a top site link click for "typed" frecency bonus as 8583 // specified as a property on the link. 8584 event.preventDefault(); 8585 const { 8586 altKey, 8587 button, 8588 ctrlKey, 8589 metaKey, 8590 shiftKey 8591 } = event; 8592 if (!this.props.link.searchTopSite) { 8593 this.props.dispatch(actionCreators.OnlyToMain({ 8594 type: actionTypes.OPEN_LINK, 8595 data: Object.assign(this.props.link, { 8596 event: { 8597 altKey, 8598 button, 8599 ctrlKey, 8600 metaKey, 8601 shiftKey 8602 }, 8603 is_sponsored: !!this.props.link.sponsored_tile_id 8604 }) 8605 })); 8606 if (this.props.link.type === SPOC_TYPE) { 8607 // Record a Pocket-specific click. 8608 this.props.dispatch(actionCreators.ImpressionStats({ 8609 source: TOP_SITES_SOURCE, 8610 click: 0, 8611 tiles: [{ 8612 id: this.props.link.id, 8613 pos: this.props.link.pos, 8614 shim: this.props.link.shim && this.props.link.shim.click 8615 }] 8616 })); 8617 8618 // Record a click for a Pocket sponsored tile. 8619 // This first event is for the shim property 8620 // and is used by our ad service provider. 8621 this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({ 8622 event: "CLICK", 8623 source: TOP_SITES_SOURCE, 8624 action_position: this.props.link.pos, 8625 value: { 8626 card_type: "spoc", 8627 tile_id: this.props.link.id, 8628 shim: this.props.link.shim && this.props.link.shim.click, 8629 attribution: this.props.link.attribution 8630 } 8631 })); 8632 8633 // A second event is recoded for internal usage. 8634 const title = this.props.link.label || this.props.link.hostname; 8635 this.props.dispatch(actionCreators.OnlyToMain({ 8636 type: actionTypes.TOP_SITES_SPONSORED_IMPRESSION_STATS, 8637 data: { 8638 type: "click", 8639 position: this.props.link.pos, 8640 tile_id: this.props.link.id, 8641 advertiser: title.toLocaleLowerCase(), 8642 source: NEWTAB_SOURCE, 8643 attribution: this.props.link.attribution 8644 } 8645 })); 8646 } else if (isSponsored(this.props.link)) { 8647 // Record a click for a non-Pocket sponsored tile. 8648 const title = this.props.link.label || this.props.link.hostname; 8649 this.props.dispatch(actionCreators.OnlyToMain({ 8650 type: actionTypes.TOP_SITES_SPONSORED_IMPRESSION_STATS, 8651 data: { 8652 type: "click", 8653 position: this.props.index, 8654 tile_id: this.props.link.sponsored_tile_id || -1, 8655 reporting_url: this.props.link.sponsored_click_url, 8656 advertiser: title.toLocaleLowerCase(), 8657 source: NEWTAB_SOURCE, 8658 visible_topsites: this.props.visibleTopSites, 8659 frecency_boosted: this.props.link.type === "frecency-boost", 8660 attribution: this.props.link.attribution 8661 } 8662 })); 8663 } else { 8664 // Record a click for an organic tile. 8665 this.props.dispatch(actionCreators.OnlyToMain({ 8666 type: actionTypes.TOP_SITES_ORGANIC_IMPRESSION_STATS, 8667 data: { 8668 type: "click", 8669 position: this.props.index, 8670 source: NEWTAB_SOURCE, 8671 isPinned: this.props.link.isPinned, 8672 guid: this.props.link.guid, 8673 visible_topsites: this.props.visibleTopSites, 8674 smartScores: this.props.link.scores, 8675 smartWeights: this.props.link.weights 8676 } 8677 })); 8678 } 8679 if (this.props.link.sendAttributionRequest) { 8680 this.props.dispatch(actionCreators.OnlyToMain({ 8681 type: actionTypes.PARTNER_LINK_ATTRIBUTION, 8682 data: { 8683 targetURL: this.props.link.url, 8684 source: "newtab" 8685 } 8686 })); 8687 } 8688 } else { 8689 this.props.dispatch(actionCreators.OnlyToMain({ 8690 type: actionTypes.FILL_SEARCH_TERM, 8691 data: { 8692 label: this.props.link.label 8693 } 8694 })); 8695 } 8696 } 8697 onMenuUpdate(isOpen) { 8698 if (isOpen) { 8699 this.props.onActivate(this.props.index); 8700 } else { 8701 this.props.onActivate(); 8702 } 8703 } 8704 render() { 8705 const { 8706 props 8707 } = this; 8708 const { 8709 link 8710 } = props; 8711 const isContextMenuOpen = props.activeIndex === props.index; 8712 const title = link.label || link.title || link.hostname; 8713 let menuOptions; 8714 if (link.sponsored_position) { 8715 menuOptions = TOP_SITES_SPONSORED_POSITION_CONTEXT_MENU_OPTIONS; 8716 } else if (link.searchTopSite) { 8717 menuOptions = TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS; 8718 } else if (link.type === SPOC_TYPE) { 8719 menuOptions = TOP_SITES_SPOC_CONTEXT_MENU_OPTIONS; 8720 } else { 8721 menuOptions = TOP_SITES_CONTEXT_MENU_OPTIONS; 8722 } 8723 return /*#__PURE__*/external_React_default().createElement(TopSiteLink, TopSite_extends({}, props, { 8724 onClick: this.onLinkClick, 8725 onDragEvent: this.props.onDragEvent, 8726 className: `${props.className || ""}${isContextMenuOpen ? " active" : ""}`, 8727 title: title, 8728 setPref: this.props.setPref, 8729 tabIndex: this.props.tabIndex, 8730 onFocus: this.props.onFocus 8731 }), /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement(ContextMenuButton, { 8732 tooltip: "newtab-menu-content-tooltip", 8733 tooltipArgs: { 8734 title 8735 }, 8736 onUpdate: this.onMenuUpdate, 8737 tabIndex: this.props.tabIndex, 8738 onFocus: this.props.onFocus 8739 }, /*#__PURE__*/external_React_default().createElement(LinkMenu, { 8740 dispatch: props.dispatch, 8741 index: props.index, 8742 onUpdate: this.onMenuUpdate, 8743 options: menuOptions, 8744 site: link, 8745 shouldSendImpressionStats: link.type === SPOC_TYPE, 8746 siteInfo: this._getTelemetryInfo(), 8747 source: TOP_SITES_SOURCE 8748 })))); 8749 } 8750 } 8751 TopSite.defaultProps = { 8752 link: {}, 8753 onActivate() {} 8754 }; 8755 class TopSiteAddButton extends (external_React_default()).PureComponent { 8756 constructor(props) { 8757 super(props); 8758 this.onEditButtonClick = this.onEditButtonClick.bind(this); 8759 } 8760 onEditButtonClick() { 8761 this.props.dispatch({ 8762 type: actionTypes.TOP_SITES_EDIT, 8763 data: { 8764 index: this.props.index 8765 } 8766 }); 8767 } 8768 render() { 8769 return /*#__PURE__*/external_React_default().createElement(TopSiteLink, TopSite_extends({}, this.props, { 8770 isAddButton: true, 8771 className: `add-button ${this.props.className || ""}`, 8772 onClick: this.onEditButtonClick, 8773 setPref: this.props.setPref, 8774 isDraggable: false, 8775 tabIndex: this.props.tabIndex 8776 })); 8777 } 8778 } 8779 class TopSitePlaceholder extends (external_React_default()).PureComponent { 8780 render() { 8781 return /*#__PURE__*/external_React_default().createElement(TopSiteLink, TopSite_extends({}, this.props, { 8782 className: `placeholder ${this.props.className || ""}`, 8783 isDraggable: false 8784 })); 8785 } 8786 } 8787 class _TopSiteList extends (external_React_default()).PureComponent { 8788 static get DEFAULT_STATE() { 8789 return { 8790 activeIndex: null, 8791 draggedIndex: null, 8792 draggedSite: null, 8793 draggedTitle: null, 8794 topSitesPreview: null, 8795 focusedIndex: 0 8796 }; 8797 } 8798 constructor(props) { 8799 super(props); 8800 this.state = _TopSiteList.DEFAULT_STATE; 8801 this.onDragEvent = this.onDragEvent.bind(this); 8802 this.onActivate = this.onActivate.bind(this); 8803 this.onWrapperFocus = this.onWrapperFocus.bind(this); 8804 this.onTopsiteFocus = this.onTopsiteFocus.bind(this); 8805 this.onWrapperBlur = this.onWrapperBlur.bind(this); 8806 this.onKeyDown = this.onKeyDown.bind(this); 8807 } 8808 componentWillReceiveProps(nextProps) { 8809 if (this.state.draggedSite) { 8810 const prevTopSites = this.props.TopSites && this.props.TopSites.rows; 8811 const newTopSites = nextProps.TopSites && nextProps.TopSites.rows; 8812 if (prevTopSites && prevTopSites[this.state.draggedIndex] && prevTopSites[this.state.draggedIndex].url === this.state.draggedSite.url && (!newTopSites[this.state.draggedIndex] || newTopSites[this.state.draggedIndex].url !== this.state.draggedSite.url)) { 8813 // We got the new order from the redux store via props. We can clear state now. 8814 this.setState(_TopSiteList.DEFAULT_STATE); 8815 } 8816 } 8817 } 8818 userEvent(event, index) { 8819 this.props.dispatch(actionCreators.UserEvent({ 8820 event, 8821 source: TOP_SITES_SOURCE, 8822 action_position: index 8823 })); 8824 } 8825 onDragEvent(event, index, link, title) { 8826 switch (event.type) { 8827 case "dragstart": 8828 this.dropped = false; 8829 this.setState({ 8830 draggedIndex: index, 8831 draggedSite: link, 8832 draggedTitle: title, 8833 activeIndex: null 8834 }); 8835 this.userEvent("DRAG", index); 8836 break; 8837 case "dragend": 8838 if (!this.dropped) { 8839 // If there was no drop event, reset the state to the default. 8840 this.setState(_TopSiteList.DEFAULT_STATE); 8841 } 8842 break; 8843 case "dragenter": 8844 if (index === this.state.draggedIndex) { 8845 this.setState({ 8846 topSitesPreview: null 8847 }); 8848 } else { 8849 this.setState({ 8850 topSitesPreview: this._makeTopSitesPreview(index) 8851 }); 8852 } 8853 break; 8854 case "drop": 8855 if (index !== this.state.draggedIndex) { 8856 this.dropped = true; 8857 this.props.dispatch(actionCreators.AlsoToMain({ 8858 type: actionTypes.TOP_SITES_INSERT, 8859 data: { 8860 site: { 8861 url: this.state.draggedSite.url, 8862 label: this.state.draggedTitle, 8863 customScreenshotURL: this.state.draggedSite.customScreenshotURL, 8864 // Only if the search topsites experiment is enabled 8865 ...(this.state.draggedSite.searchTopSite && { 8866 searchTopSite: true 8867 }) 8868 }, 8869 index, 8870 draggedFromIndex: this.state.draggedIndex 8871 } 8872 })); 8873 this.userEvent("DROP", index); 8874 } 8875 break; 8876 } 8877 } 8878 _getTopSites() { 8879 // Make a copy of the sites to truncate or extend to desired length 8880 let topSites = this.props.TopSites.rows.slice(); 8881 topSites.length = this.props.TopSitesRows * TOP_SITES_MAX_SITES_PER_ROW; 8882 // if topSites do not fill an entire row add 'Add shortcut' button to array of topSites 8883 // (there should only be one of these) 8884 let firstPlaceholder = topSites.findIndex(Object.is.bind(null, undefined)); 8885 // make sure placeholder exists and there already isnt a add button 8886 if (firstPlaceholder && !topSites.includes(site => site.isAddButton)) { 8887 topSites[firstPlaceholder] = { 8888 isAddButton: true 8889 }; 8890 } else if (topSites.includes(site => site.isAddButton)) { 8891 topSites.push(topSites.splice(topSites.indexOf({ 8892 isAddButton: true 8893 }), 1)[0]); 8894 } 8895 return topSites; 8896 } 8897 8898 /** 8899 * Make a preview of the topsites that will be the result of dropping the currently 8900 * dragged site at the specified index. 8901 */ 8902 _makeTopSitesPreview(index) { 8903 const topSites = this._getTopSites(); 8904 topSites[this.state.draggedIndex] = null; 8905 const preview = topSites.map(site => site && (site.isPinned || isSponsored(site)) ? site : null); 8906 const unpinned = topSites.filter(site => site && !site.isPinned && !isSponsored(site)); 8907 const siteToInsert = Object.assign({}, this.state.draggedSite, { 8908 isPinned: true, 8909 isDragged: true 8910 }); 8911 if (!preview[index]) { 8912 preview[index] = siteToInsert; 8913 } else { 8914 // Find the hole to shift the pinned site(s) towards. We shift towards the 8915 // hole left by the site being dragged. 8916 let holeIndex = index; 8917 const indexStep = index > this.state.draggedIndex ? -1 : 1; 8918 while (preview[holeIndex]) { 8919 holeIndex += indexStep; 8920 } 8921 8922 // Shift towards the hole. 8923 const shiftingStep = index > this.state.draggedIndex ? 1 : -1; 8924 while (index > this.state.draggedIndex ? holeIndex < index : holeIndex > index) { 8925 let nextIndex = holeIndex + shiftingStep; 8926 while (isSponsored(preview[nextIndex])) { 8927 nextIndex += shiftingStep; 8928 } 8929 preview[holeIndex] = preview[nextIndex]; 8930 holeIndex = nextIndex; 8931 } 8932 preview[index] = siteToInsert; 8933 } 8934 8935 // Fill in the remaining holes with unpinned sites. 8936 for (let i = 0; i < preview.length; i++) { 8937 if (!preview[i]) { 8938 preview[i] = unpinned.shift() || null; 8939 } 8940 } 8941 return preview; 8942 } 8943 onActivate(index) { 8944 this.setState({ 8945 activeIndex: index 8946 }); 8947 } 8948 onKeyDown(e) { 8949 if (this.state.activeIndex || this.state.activeIndex === 0) { 8950 return; 8951 } 8952 if (e.key === "ArrowLeft" || e.key === "ArrowRight") { 8953 // Arrow direction should match visual navigation direction in RTL 8954 const isRTL = document.dir === "rtl"; 8955 const navigateToPrevious = isRTL ? e.key === "ArrowRight" : e.key === "ArrowLeft"; 8956 const targetTopSite = navigateToPrevious ? this.focusedRef?.previousSibling : this.focusedRef?.nextSibling; 8957 const targetAnchor = targetTopSite?.querySelector("a"); 8958 if (targetAnchor) { 8959 targetAnchor.tabIndex = 0; 8960 targetAnchor.focus(); 8961 } 8962 } 8963 } 8964 onWrapperFocus() { 8965 this.focusRef?.addEventListener("keydown", this.onKeyDown); 8966 } 8967 onWrapperBlur() { 8968 this.focusRef?.removeEventListener("keydown", this.onKeyDown); 8969 } 8970 onTopsiteFocus(focusIndex) { 8971 this.setState(() => ({ 8972 focusedIndex: focusIndex 8973 })); 8974 } 8975 render() { 8976 const { 8977 props 8978 } = this; 8979 const topSites = this.state.topSitesPreview || this._getTopSites(); 8980 const topSitesUI = []; 8981 const commonProps = { 8982 onDragEvent: this.onDragEvent, 8983 dispatch: props.dispatch 8984 }; 8985 // We assign a key to each placeholder slot. We need it to be independent 8986 // of the slot index (i below) so that the keys used stay the same during 8987 // drag and drop reordering and the underlying DOM nodes are reused. 8988 // This mostly (only?) affects linux so be sure to test on linux before changing. 8989 let holeIndex = 0; 8990 8991 // On narrow viewports, we only show 6 sites per row. We'll mark the rest as 8992 // .hide-for-narrow to hide in CSS via @media query. 8993 const maxNarrowVisibleIndex = props.TopSitesRows * 6; 8994 for (let i = 0, l = topSites.length; i < l; i++) { 8995 const link = topSites[i] && Object.assign({}, topSites[i], { 8996 iconType: this.props.topSiteIconType(topSites[i]) 8997 }); 8998 const slotProps = { 8999 key: link ? link.url : holeIndex++, 9000 index: i 9001 }; 9002 if (i >= maxNarrowVisibleIndex) { 9003 slotProps.className = "hide-for-narrow"; 9004 } 9005 let topSiteLink; 9006 // Use a placeholder if the link is empty or it's rendering a sponsored 9007 // tile for the about:home startup cache. 9008 if (!link || props.App.isForStartupCache.TopSites && isSponsored(link)) { 9009 if (link) { 9010 topSiteLink = /*#__PURE__*/external_React_default().createElement(TopSitePlaceholder, TopSite_extends({}, slotProps, commonProps)); 9011 } 9012 } else if (topSites[i]?.isAddButton) { 9013 topSiteLink = /*#__PURE__*/external_React_default().createElement(TopSiteAddButton, TopSite_extends({}, slotProps, commonProps, { 9014 setRef: i === this.state.focusedIndex ? el => { 9015 this.focusedRef = el; 9016 } : () => {}, 9017 tabIndex: i === this.state.focusedIndex ? 0 : -1, 9018 onFocus: () => { 9019 this.onTopsiteFocus(i); 9020 }, 9021 Messages: this.props.Messages, 9022 visibleTopSites: this.props.visibleTopSites 9023 })); 9024 } else { 9025 topSiteLink = /*#__PURE__*/external_React_default().createElement(TopSite, TopSite_extends({ 9026 link: link, 9027 activeIndex: this.state.activeIndex, 9028 onActivate: this.onActivate 9029 }, slotProps, commonProps, { 9030 colors: props.colors, 9031 setRef: i === this.state.focusedIndex ? el => { 9032 this.focusedRef = el; 9033 } : () => {}, 9034 tabIndex: i === this.state.focusedIndex ? 0 : -1, 9035 onFocus: () => { 9036 this.onTopsiteFocus(i); 9037 }, 9038 visibleTopSites: this.props.visibleTopSites 9039 })); 9040 } 9041 topSitesUI.push(topSiteLink); 9042 } 9043 return /*#__PURE__*/external_React_default().createElement("div", { 9044 className: "top-sites-list-wrapper" 9045 }, /*#__PURE__*/external_React_default().createElement("ul", { 9046 role: "group", 9047 "aria-label": "Shortcuts", 9048 onFocus: this.onWrapperFocus, 9049 onBlur: this.onWrapperBlur, 9050 ref: el => { 9051 this.focusRef = el; 9052 }, 9053 className: `top-sites-list${this.state.draggedSite ? " dnd-active" : ""}` 9054 }, topSitesUI)); 9055 } 9056 } 9057 const TopSiteList = (0,external_ReactRedux_namespaceObject.connect)(state => ({ 9058 App: state.App, 9059 Messages: state.Messages, 9060 Prefs: state.Prefs 9061 }))(_TopSiteList); 9062 ;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteForm.jsx 9063 /* This Source Code Form is subject to the terms of the Mozilla Public 9064 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 9065 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 9066 9067 9068 9069 9070 9071 9072 9073 class TopSiteForm extends (external_React_default()).PureComponent { 9074 constructor(props) { 9075 super(props); 9076 const { 9077 site 9078 } = props; 9079 this.state = { 9080 label: site ? site.label || site.hostname : "", 9081 url: site ? site.url : "", 9082 validationError: false, 9083 customScreenshotUrl: site ? site.customScreenshotURL : "", 9084 showCustomScreenshotForm: site ? site.customScreenshotURL : false, 9085 hasURLChanged: false, 9086 hasTitleChanged: false 9087 }; 9088 this.onClearScreenshotInput = this.onClearScreenshotInput.bind(this); 9089 this.onLabelChange = this.onLabelChange.bind(this); 9090 this.onUrlChange = this.onUrlChange.bind(this); 9091 this.onCancelButtonClick = this.onCancelButtonClick.bind(this); 9092 this.onClearUrlClick = this.onClearUrlClick.bind(this); 9093 this.onDoneButtonClick = this.onDoneButtonClick.bind(this); 9094 this.onCustomScreenshotUrlChange = this.onCustomScreenshotUrlChange.bind(this); 9095 this.onPreviewButtonClick = this.onPreviewButtonClick.bind(this); 9096 this.onEnableScreenshotUrlForm = this.onEnableScreenshotUrlForm.bind(this); 9097 this.validateUrl = this.validateUrl.bind(this); 9098 } 9099 onLabelChange(event) { 9100 this.setState({ 9101 label: event.target.value, 9102 hasTitleChanged: true 9103 }); 9104 } 9105 onUrlChange(event) { 9106 this.setState({ 9107 url: event.target.value, 9108 validationError: false, 9109 hasURLChanged: true 9110 }); 9111 } 9112 onClearUrlClick() { 9113 this.setState({ 9114 url: "", 9115 validationError: false 9116 }); 9117 } 9118 onEnableScreenshotUrlForm() { 9119 this.setState({ 9120 showCustomScreenshotForm: true 9121 }); 9122 } 9123 _updateCustomScreenshotInput(customScreenshotUrl) { 9124 this.setState({ 9125 customScreenshotUrl, 9126 validationError: false 9127 }); 9128 this.props.dispatch({ 9129 type: actionTypes.PREVIEW_REQUEST_CANCEL 9130 }); 9131 } 9132 onCustomScreenshotUrlChange(event) { 9133 this._updateCustomScreenshotInput(event.target.value); 9134 } 9135 onClearScreenshotInput() { 9136 this._updateCustomScreenshotInput(""); 9137 } 9138 onCancelButtonClick(ev) { 9139 ev.preventDefault(); 9140 this.props.onClose(); 9141 } 9142 onDoneButtonClick(ev) { 9143 ev.preventDefault(); 9144 if (this.validateForm()) { 9145 const site = { 9146 url: this.cleanUrl(this.state.url) 9147 }; 9148 const { 9149 index 9150 } = this.props; 9151 const isEdit = !!this.props.site; 9152 if (this.state.label !== "") { 9153 site.label = this.state.label; 9154 } 9155 if (this.state.customScreenshotUrl) { 9156 site.customScreenshotURL = this.cleanUrl(this.state.customScreenshotUrl); 9157 } else if (this.props.site && this.props.site.customScreenshotURL) { 9158 // Used to flag that previously cached screenshot should be removed 9159 site.customScreenshotURL = null; 9160 } 9161 this.props.dispatch(actionCreators.AlsoToMain({ 9162 type: actionTypes.TOP_SITES_PIN, 9163 data: { 9164 site, 9165 index 9166 } 9167 })); 9168 if (isEdit) { 9169 this.props.dispatch(actionCreators.UserEvent({ 9170 source: TOP_SITES_SOURCE, 9171 event: "TOP_SITES_EDIT", 9172 action_position: index, 9173 hasTitleChanged: this.state.hasTitleChanged, 9174 hasURLChanged: this.state.hasURLChanged 9175 })); 9176 } else if (!isEdit) { 9177 this.props.dispatch(actionCreators.UserEvent({ 9178 source: TOP_SITES_SOURCE, 9179 event: "TOP_SITES_ADD", 9180 action_position: index 9181 })); 9182 } 9183 this.props.onClose(); 9184 } 9185 } 9186 onPreviewButtonClick(event) { 9187 event.preventDefault(); 9188 if (this.validateForm()) { 9189 this.props.dispatch(actionCreators.AlsoToMain({ 9190 type: actionTypes.PREVIEW_REQUEST, 9191 data: { 9192 url: this.cleanUrl(this.state.customScreenshotUrl) 9193 } 9194 })); 9195 this.props.dispatch(actionCreators.UserEvent({ 9196 source: TOP_SITES_SOURCE, 9197 event: "PREVIEW_REQUEST" 9198 })); 9199 } 9200 } 9201 cleanUrl(url) { 9202 // If we are missing a protocol, prepend http:// 9203 if (!url.startsWith("http:") && !url.startsWith("https:")) { 9204 return `http://${url}`; 9205 } 9206 return url; 9207 } 9208 _tryParseUrl(url) { 9209 try { 9210 return new URL(url); 9211 } catch (e) { 9212 return null; 9213 } 9214 } 9215 validateUrl(url) { 9216 const validProtocols = ["http:", "https:"]; 9217 const urlObj = this._tryParseUrl(url) || this._tryParseUrl(this.cleanUrl(url)); 9218 return urlObj && validProtocols.includes(urlObj.protocol); 9219 } 9220 validateCustomScreenshotUrl() { 9221 const { 9222 customScreenshotUrl 9223 } = this.state; 9224 return !customScreenshotUrl || this.validateUrl(customScreenshotUrl); 9225 } 9226 validateForm() { 9227 const validate = this.validateUrl(this.state.url) && this.validateCustomScreenshotUrl(); 9228 if (!validate) { 9229 this.setState({ 9230 validationError: true 9231 }); 9232 } 9233 return validate; 9234 } 9235 _renderCustomScreenshotInput() { 9236 const { 9237 customScreenshotUrl 9238 } = this.state; 9239 const requestFailed = this.props.previewResponse === ""; 9240 const validationError = this.state.validationError && !this.validateCustomScreenshotUrl() || requestFailed; 9241 // Set focus on error if the url field is valid or when the input is first rendered and is empty 9242 const shouldFocus = validationError && this.validateUrl(this.state.url) || !customScreenshotUrl; 9243 const isLoading = this.props.previewResponse === null && customScreenshotUrl && this.props.previewUrl === this.cleanUrl(customScreenshotUrl); 9244 if (!this.state.showCustomScreenshotForm) { 9245 return /*#__PURE__*/external_React_default().createElement(A11yLinkButton, { 9246 onClick: this.onEnableScreenshotUrlForm, 9247 className: "enable-custom-image-input", 9248 "data-l10n-id": "newtab-topsites-use-image-link" 9249 }); 9250 } 9251 return /*#__PURE__*/external_React_default().createElement("div", { 9252 className: "custom-image-input-container" 9253 }, /*#__PURE__*/external_React_default().createElement(TopSiteFormInput, { 9254 errorMessageId: requestFailed ? "newtab-topsites-image-validation" : "newtab-topsites-url-validation", 9255 loading: isLoading, 9256 onChange: this.onCustomScreenshotUrlChange, 9257 onClear: this.onClearScreenshotInput, 9258 shouldFocus: shouldFocus, 9259 typeUrl: true, 9260 value: customScreenshotUrl, 9261 validationError: validationError, 9262 titleId: "newtab-topsites-image-url-label", 9263 placeholderId: "newtab-topsites-url-input" 9264 })); 9265 } 9266 render() { 9267 const { 9268 customScreenshotUrl 9269 } = this.state; 9270 const requestFailed = this.props.previewResponse === ""; 9271 // For UI purposes, editing without an existing link is "add" 9272 const showAsAdd = !this.props.site; 9273 const previous = this.props.site && this.props.site.customScreenshotURL || ""; 9274 const changed = customScreenshotUrl && this.cleanUrl(customScreenshotUrl) !== previous; 9275 // Preview mode if changes were made to the custom screenshot URL and no preview was received yet 9276 // or the request failed 9277 const previewMode = changed && !this.props.previewResponse; 9278 const previewLink = Object.assign({}, this.props.site); 9279 if (this.props.previewResponse) { 9280 previewLink.screenshot = this.props.previewResponse; 9281 previewLink.customScreenshotURL = this.props.previewUrl; 9282 } 9283 // Handles the form submit so an enter press performs the correct action 9284 const onSubmit = previewMode ? this.onPreviewButtonClick : this.onDoneButtonClick; 9285 const addTopsitesHeaderL10nId = "newtab-topsites-add-shortcut-header"; 9286 const editTopsitesHeaderL10nId = "newtab-topsites-edit-shortcut-header"; 9287 return /*#__PURE__*/external_React_default().createElement("form", { 9288 className: "topsite-form", 9289 onSubmit: onSubmit 9290 }, /*#__PURE__*/external_React_default().createElement("div", { 9291 className: "form-input-container" 9292 }, /*#__PURE__*/external_React_default().createElement("h3", { 9293 className: "section-title grey-title", 9294 "data-l10n-id": showAsAdd ? addTopsitesHeaderL10nId : editTopsitesHeaderL10nId 9295 }), /*#__PURE__*/external_React_default().createElement("div", { 9296 className: "fields-and-preview" 9297 }, /*#__PURE__*/external_React_default().createElement("div", { 9298 className: "form-wrapper" 9299 }, /*#__PURE__*/external_React_default().createElement(TopSiteFormInput, { 9300 onChange: this.onLabelChange, 9301 value: this.state.label, 9302 titleId: "newtab-topsites-title-label", 9303 placeholderId: "newtab-topsites-title-input", 9304 autoFocusOnOpen: true 9305 }), /*#__PURE__*/external_React_default().createElement(TopSiteFormInput, { 9306 onChange: this.onUrlChange, 9307 shouldFocus: this.state.validationError && !this.validateUrl(this.state.url), 9308 value: this.state.url, 9309 onClear: this.onClearUrlClick, 9310 validationError: this.state.validationError && !this.validateUrl(this.state.url), 9311 titleId: "newtab-topsites-url-label", 9312 typeUrl: true, 9313 placeholderId: "newtab-topsites-url-input", 9314 errorMessageId: "newtab-topsites-url-validation" 9315 }), this._renderCustomScreenshotInput()), /*#__PURE__*/external_React_default().createElement(TopSiteLink, { 9316 link: previewLink, 9317 defaultStyle: requestFailed, 9318 title: this.state.label 9319 }))), /*#__PURE__*/external_React_default().createElement("section", { 9320 className: "actions" 9321 }, /*#__PURE__*/external_React_default().createElement("button", { 9322 className: "cancel", 9323 type: "button", 9324 onClick: this.onCancelButtonClick, 9325 "data-l10n-id": "newtab-topsites-cancel-button" 9326 }), previewMode ? /*#__PURE__*/external_React_default().createElement("button", { 9327 className: "done preview", 9328 type: "submit", 9329 "data-l10n-id": "newtab-topsites-preview-button" 9330 }) : /*#__PURE__*/external_React_default().createElement("button", { 9331 className: "done", 9332 type: "submit", 9333 "data-l10n-id": showAsAdd ? "newtab-topsites-add-button" : "newtab-topsites-save-button" 9334 }))); 9335 } 9336 } 9337 TopSiteForm.defaultProps = { 9338 site: null, 9339 index: -1 9340 }; 9341 ;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSites.jsx 9342 function TopSites_extends() { return TopSites_extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, TopSites_extends.apply(null, arguments); } 9343 /* This Source Code Form is subject to the terms of the Mozilla Public 9344 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 9345 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 9346 9347 9348 9349 9350 9351 9352 9353 9354 9355 9356 9357 9358 function topSiteIconType(link) { 9359 if (link.customScreenshotURL) { 9360 return "custom_screenshot"; 9361 } 9362 if (link.tippyTopIcon || link.faviconRef === "tippytop") { 9363 return "tippytop"; 9364 } 9365 if (link.faviconSize >= MIN_RICH_FAVICON_SIZE) { 9366 return "rich_icon"; 9367 } 9368 if (link.screenshot) { 9369 return "screenshot"; 9370 } 9371 return "no_image"; 9372 } 9373 9374 /** 9375 * Iterates through TopSites and counts types of images. 9376 * 9377 * @param acc Accumulator for reducer. 9378 * @param topsite Entry in TopSites. 9379 */ 9380 function countTopSitesIconsTypes(topSites) { 9381 const countTopSitesTypes = (acc, link) => { 9382 acc[topSiteIconType(link)]++; 9383 return acc; 9384 }; 9385 return topSites.reduce(countTopSitesTypes, { 9386 custom_screenshot: 0, 9387 screenshot: 0, 9388 tippytop: 0, 9389 rich_icon: 0, 9390 no_image: 0 9391 }); 9392 } 9393 class _TopSites extends (external_React_default()).PureComponent { 9394 constructor(props) { 9395 super(props); 9396 this.onEditFormClose = this.onEditFormClose.bind(this); 9397 this.onSearchShortcutsFormClose = this.onSearchShortcutsFormClose.bind(this); 9398 } 9399 9400 /** 9401 * Dispatch session statistics about the quality of TopSites icons and pinned count. 9402 */ 9403 _dispatchTopSitesStats() { 9404 const topSites = this._getVisibleTopSites().filter(topSite => topSite !== null && topSite !== undefined); 9405 const topSitesIconsStats = countTopSitesIconsTypes(topSites); 9406 const topSitesPinned = topSites.filter(site => !!site.isPinned).length; 9407 const searchShortcuts = topSites.filter(site => !!site.searchTopSite).length; 9408 // Dispatch telemetry event with the count of TopSites images types. 9409 this.props.dispatch(actionCreators.AlsoToMain({ 9410 type: actionTypes.SAVE_SESSION_PERF_DATA, 9411 data: { 9412 topsites_icon_stats: topSitesIconsStats, 9413 topsites_pinned: topSitesPinned, 9414 topsites_search_shortcuts: searchShortcuts 9415 } 9416 })); 9417 } 9418 9419 /** 9420 * Return the TopSites that are visible based on prefs and window width. 9421 */ 9422 _getVisibleTopSites() { 9423 // We hide 2 sites per row when not in the wide layout. 9424 let sitesPerRow = TOP_SITES_MAX_SITES_PER_ROW; 9425 // $break-point-widest = 1072px (from _variables.scss) 9426 if (!globalThis.matchMedia(`(min-width: 1072px)`).matches) { 9427 sitesPerRow -= 2; 9428 } 9429 return this.props.TopSites.rows.slice(0, this.props.TopSitesRows * sitesPerRow); 9430 } 9431 componentDidUpdate() { 9432 this._dispatchTopSitesStats(); 9433 } 9434 componentDidMount() { 9435 this._dispatchTopSitesStats(); 9436 } 9437 onEditFormClose() { 9438 this.props.dispatch(actionCreators.UserEvent({ 9439 source: TOP_SITES_SOURCE, 9440 event: "TOP_SITES_EDIT_CLOSE" 9441 })); 9442 this.props.dispatch({ 9443 type: actionTypes.TOP_SITES_CANCEL_EDIT 9444 }); 9445 } 9446 onSearchShortcutsFormClose() { 9447 this.props.dispatch(actionCreators.UserEvent({ 9448 source: TOP_SITES_SOURCE, 9449 event: "SEARCH_EDIT_CLOSE" 9450 })); 9451 this.props.dispatch({ 9452 type: actionTypes.TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL 9453 }); 9454 } 9455 render() { 9456 const { 9457 props 9458 } = this; 9459 const { 9460 editForm, 9461 showSearchShortcutsForm 9462 } = props.TopSites; 9463 const extraMenuOptions = ["AddTopSite"]; 9464 let visibleTopSites; 9465 const colors = props.Prefs.values["newNewtabExperience.colors"]; 9466 9467 // do not run this function when for startup cache 9468 if (!props.App.isForStartupCache.TopSites) { 9469 visibleTopSites = this._getVisibleTopSites()?.length; 9470 } 9471 if (props.Prefs.values["improvesearch.topSiteSearchShortcuts"]) { 9472 extraMenuOptions.push("AddSearchShortcut"); 9473 } 9474 return /*#__PURE__*/external_React_default().createElement(ComponentPerfTimer, { 9475 id: "topsites", 9476 initialized: props.TopSites.initialized, 9477 dispatch: props.dispatch 9478 }, /*#__PURE__*/external_React_default().createElement(CollapsibleSection, { 9479 className: "top-sites", 9480 id: "topsites", 9481 title: props.title || { 9482 id: "newtab-section-header-topsites" 9483 }, 9484 hideTitle: true, 9485 extraMenuOptions: extraMenuOptions, 9486 showPrefName: "feeds.topsites", 9487 eventSource: TOP_SITES_SOURCE, 9488 collapsed: false, 9489 isFixed: props.isFixed, 9490 isFirst: props.isFirst, 9491 isLast: props.isLast, 9492 dispatch: props.dispatch 9493 }, /*#__PURE__*/external_React_default().createElement(TopSiteList, { 9494 TopSites: props.TopSites, 9495 TopSitesRows: props.TopSitesRows, 9496 dispatch: props.dispatch, 9497 topSiteIconType: topSiteIconType, 9498 colors: colors, 9499 visibleTopSites: visibleTopSites 9500 }), /*#__PURE__*/external_React_default().createElement("div", { 9501 className: "edit-topsites-wrapper" 9502 }, editForm && /*#__PURE__*/external_React_default().createElement("div", { 9503 className: "edit-topsites" 9504 }, /*#__PURE__*/external_React_default().createElement(ModalOverlayWrapper, { 9505 unstyled: true, 9506 onClose: this.onEditFormClose, 9507 innerClassName: "modal" 9508 }, /*#__PURE__*/external_React_default().createElement(TopSiteForm, TopSites_extends({ 9509 site: props.TopSites.rows[editForm.index], 9510 onClose: this.onEditFormClose, 9511 dispatch: this.props.dispatch 9512 }, editForm)))), showSearchShortcutsForm && /*#__PURE__*/external_React_default().createElement("div", { 9513 className: "edit-search-shortcuts" 9514 }, /*#__PURE__*/external_React_default().createElement(ModalOverlayWrapper, { 9515 unstyled: true, 9516 onClose: this.onSearchShortcutsFormClose, 9517 innerClassName: "modal" 9518 }, /*#__PURE__*/external_React_default().createElement(SearchShortcutsForm, { 9519 TopSites: props.TopSites, 9520 onClose: this.onSearchShortcutsFormClose, 9521 dispatch: this.props.dispatch 9522 })))))); 9523 } 9524 } 9525 const TopSites_TopSites = (0,external_ReactRedux_namespaceObject.connect)(state => ({ 9526 App: state.App, 9527 TopSites: state.TopSites, 9528 Prefs: state.Prefs, 9529 TopSitesRows: state.Prefs.values.topSitesRows 9530 }))(_TopSites); 9531 ;// CONCATENATED MODULE: ./content-src/components/Sections/Sections.jsx 9532 function Sections_extends() { return Sections_extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, Sections_extends.apply(null, arguments); } 9533 /* This Source Code Form is subject to the terms of the Mozilla Public 9534 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 9535 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 9536 9537 9538 9539 9540 9541 9542 9543 9544 9545 9546 const Sections_VISIBLE = "visible"; 9547 const Sections_VISIBILITY_CHANGE_EVENT = "visibilitychange"; 9548 const CARDS_PER_ROW_DEFAULT = 3; 9549 const CARDS_PER_ROW_COMPACT_WIDE = 4; 9550 class Section extends (external_React_default()).PureComponent { 9551 get numRows() { 9552 const { 9553 rowsPref, 9554 maxRows, 9555 Prefs 9556 } = this.props; 9557 return rowsPref ? Prefs.values[rowsPref] : maxRows; 9558 } 9559 _dispatchImpressionStats() { 9560 const { 9561 props 9562 } = this; 9563 let cardsPerRow = CARDS_PER_ROW_DEFAULT; 9564 if (props.compactCards && globalThis.matchMedia(`(min-width: 1072px)`).matches) { 9565 // If the section has compact cards and the viewport is wide enough, we show 9566 // 4 columns instead of 3. 9567 // $break-point-widest = 1072px (from _variables.scss) 9568 cardsPerRow = CARDS_PER_ROW_COMPACT_WIDE; 9569 } 9570 const maxCards = cardsPerRow * this.numRows; 9571 const cards = props.rows.slice(0, maxCards); 9572 if (this.needsImpressionStats(cards)) { 9573 props.dispatch(actionCreators.ImpressionStats({ 9574 source: props.eventSource, 9575 tiles: cards.map(link => ({ 9576 id: link.guid 9577 })) 9578 })); 9579 this.impressionCardGuids = cards.map(link => link.guid); 9580 } 9581 } 9582 9583 // This sends an event when a user sees a set of new content. If content 9584 // changes while the page is hidden (i.e. preloaded or on a hidden tab), 9585 // only send the event if the page becomes visible again. 9586 sendImpressionStatsOrAddListener() { 9587 const { 9588 props 9589 } = this; 9590 if (!props.shouldSendImpressionStats || !props.dispatch) { 9591 return; 9592 } 9593 if (props.document.visibilityState === Sections_VISIBLE) { 9594 this._dispatchImpressionStats(); 9595 } else { 9596 // We should only ever send the latest impression stats ping, so remove any 9597 // older listeners. 9598 if (this._onVisibilityChange) { 9599 props.document.removeEventListener(Sections_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 9600 } 9601 9602 // When the page becomes visible, send the impression stats ping if the section isn't collapsed. 9603 this._onVisibilityChange = () => { 9604 if (props.document.visibilityState === Sections_VISIBLE) { 9605 if (!this.props.pref.collapsed) { 9606 this._dispatchImpressionStats(); 9607 } 9608 props.document.removeEventListener(Sections_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 9609 } 9610 }; 9611 props.document.addEventListener(Sections_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 9612 } 9613 } 9614 componentWillMount() { 9615 this.sendNewTabRehydrated(this.props.initialized); 9616 } 9617 componentDidMount() { 9618 if (this.props.rows.length && !this.props.pref.collapsed) { 9619 this.sendImpressionStatsOrAddListener(); 9620 } 9621 } 9622 componentDidUpdate(prevProps) { 9623 const { 9624 props 9625 } = this; 9626 const isCollapsed = props.pref.collapsed; 9627 const wasCollapsed = prevProps.pref.collapsed; 9628 if ( 9629 // Don't send impression stats for the empty state 9630 props.rows.length && ( 9631 // We only want to send impression stats if the content of the cards has changed 9632 // and the section is not collapsed... 9633 props.rows !== prevProps.rows && !isCollapsed || 9634 // or if we are expanding a section that was collapsed. 9635 wasCollapsed && !isCollapsed)) { 9636 this.sendImpressionStatsOrAddListener(); 9637 } 9638 } 9639 componentWillUpdate(nextProps) { 9640 this.sendNewTabRehydrated(nextProps.initialized); 9641 } 9642 componentWillUnmount() { 9643 if (this._onVisibilityChange) { 9644 this.props.document.removeEventListener(Sections_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 9645 } 9646 } 9647 needsImpressionStats(cards) { 9648 if (!this.impressionCardGuids || this.impressionCardGuids.length !== cards.length) { 9649 return true; 9650 } 9651 for (let i = 0; i < cards.length; i++) { 9652 if (cards[i].guid !== this.impressionCardGuids[i]) { 9653 return true; 9654 } 9655 } 9656 return false; 9657 } 9658 9659 // The NEW_TAB_REHYDRATED event is used to inform feeds that their 9660 // data has been consumed e.g. for counting the number of tabs that 9661 // have rendered that data. 9662 sendNewTabRehydrated(initialized) { 9663 if (initialized && !this.renderNotified) { 9664 this.props.dispatch(actionCreators.AlsoToMain({ 9665 type: actionTypes.NEW_TAB_REHYDRATED, 9666 data: {} 9667 })); 9668 this.renderNotified = true; 9669 } 9670 } 9671 render() { 9672 const { 9673 id, 9674 eventSource, 9675 title, 9676 rows, 9677 emptyState, 9678 dispatch, 9679 compactCards, 9680 read_more_endpoint, 9681 contextMenuOptions, 9682 initialized, 9683 learnMore, 9684 pref, 9685 privacyNoticeURL, 9686 isFirst, 9687 isLast 9688 } = this.props; 9689 const waitingForSpoc = id === "topstories" && this.props.Pocket.waitingForSpoc; 9690 const maxCardsPerRow = compactCards ? CARDS_PER_ROW_COMPACT_WIDE : CARDS_PER_ROW_DEFAULT; 9691 const { 9692 numRows 9693 } = this; 9694 const maxCards = maxCardsPerRow * numRows; 9695 const maxCardsOnNarrow = CARDS_PER_ROW_DEFAULT * numRows; 9696 const shouldShowReadMore = read_more_endpoint; 9697 const realRows = rows.slice(0, maxCards); 9698 9699 // The empty state should only be shown after we have initialized and there is no content. 9700 // Otherwise, we should show placeholders. 9701 const shouldShowEmptyState = initialized && !rows.length; 9702 const cards = []; 9703 if (!shouldShowEmptyState) { 9704 for (let i = 0; i < maxCards; i++) { 9705 const link = realRows[i]; 9706 // On narrow viewports, we only show 3 cards per row. We'll mark the rest as 9707 // .hide-for-narrow to hide in CSS via @media query. 9708 const className = i >= maxCardsOnNarrow ? "hide-for-narrow" : ""; 9709 let usePlaceholder = !link; 9710 // If we are in the third card and waiting for spoc, 9711 // use the placeholder. 9712 if (!usePlaceholder && i === 2 && waitingForSpoc) { 9713 usePlaceholder = true; 9714 } 9715 cards.push(!usePlaceholder ? /*#__PURE__*/external_React_default().createElement(Card, { 9716 key: i, 9717 index: i, 9718 className: className, 9719 dispatch: dispatch, 9720 link: link, 9721 contextMenuOptions: contextMenuOptions, 9722 eventSource: eventSource, 9723 shouldSendImpressionStats: this.props.shouldSendImpressionStats, 9724 isWebExtension: this.props.isWebExtension 9725 }) : /*#__PURE__*/external_React_default().createElement(PlaceholderCard, { 9726 key: i, 9727 className: className 9728 })); 9729 } 9730 } 9731 const sectionClassName = ["section", compactCards ? "compact-cards" : "normal-cards"].join(" "); 9732 9733 // <Section> <-- React component 9734 // <section> <-- HTML5 element 9735 return /*#__PURE__*/external_React_default().createElement(ComponentPerfTimer, this.props, /*#__PURE__*/external_React_default().createElement(CollapsibleSection, { 9736 className: sectionClassName, 9737 title: title, 9738 id: id, 9739 eventSource: eventSource, 9740 collapsed: this.props.pref.collapsed, 9741 showPrefName: pref && pref.feed || id, 9742 privacyNoticeURL: privacyNoticeURL, 9743 Prefs: this.props.Prefs, 9744 isFixed: this.props.isFixed, 9745 isFirst: isFirst, 9746 isLast: isLast, 9747 learnMore: learnMore, 9748 dispatch: this.props.dispatch, 9749 isWebExtension: this.props.isWebExtension 9750 }, !shouldShowEmptyState && /*#__PURE__*/external_React_default().createElement("ul", { 9751 className: "section-list", 9752 style: { 9753 padding: 0 9754 } 9755 }, cards), shouldShowEmptyState && /*#__PURE__*/external_React_default().createElement("div", { 9756 className: "section-empty-state" 9757 }, /*#__PURE__*/external_React_default().createElement("div", { 9758 className: "empty-state" 9759 }, /*#__PURE__*/external_React_default().createElement(FluentOrText, { 9760 message: emptyState.message 9761 }, /*#__PURE__*/external_React_default().createElement("p", { 9762 className: "empty-state-message" 9763 })))), id === "topstories" && /*#__PURE__*/external_React_default().createElement("div", { 9764 className: "top-stories-bottom-container" 9765 }, /*#__PURE__*/external_React_default().createElement("div", { 9766 className: "wrapper-more-recommendations" 9767 }, shouldShowReadMore && /*#__PURE__*/external_React_default().createElement(MoreRecommendations, { 9768 read_more_endpoint: read_more_endpoint 9769 }))))); 9770 } 9771 } 9772 Section.defaultProps = { 9773 document: globalThis.document, 9774 rows: [], 9775 emptyState: {}, 9776 pref: {}, 9777 title: "" 9778 }; 9779 const SectionIntl = (0,external_ReactRedux_namespaceObject.connect)(state => ({ 9780 Prefs: state.Prefs, 9781 Pocket: state.Pocket 9782 }))(Section); 9783 class _Sections extends (external_React_default()).PureComponent { 9784 renderSections() { 9785 const sections = []; 9786 const enabledSections = this.props.Sections.filter(section => section.enabled); 9787 const { 9788 sectionOrder, 9789 "feeds.topsites": showTopSites 9790 } = this.props.Prefs.values; 9791 // Enabled sections doesn't include Top Sites, so we add it if enabled. 9792 const expectedCount = enabledSections.length + ~~showTopSites; 9793 for (const sectionId of sectionOrder.split(",")) { 9794 const commonProps = { 9795 key: sectionId, 9796 isFirst: sections.length === 0, 9797 isLast: sections.length === expectedCount - 1 9798 }; 9799 if (sectionId === "topsites" && showTopSites) { 9800 sections.push(/*#__PURE__*/external_React_default().createElement(TopSites_TopSites, commonProps)); 9801 } else { 9802 const section = enabledSections.find(s => s.id === sectionId); 9803 if (section) { 9804 sections.push(/*#__PURE__*/external_React_default().createElement(SectionIntl, Sections_extends({}, section, commonProps))); 9805 } 9806 } 9807 } 9808 return sections; 9809 } 9810 render() { 9811 return /*#__PURE__*/external_React_default().createElement("div", { 9812 className: "sections-list" 9813 }, this.renderSections()); 9814 } 9815 } 9816 const Sections_Sections = (0,external_ReactRedux_namespaceObject.connect)(state => ({ 9817 Sections: state.Sections, 9818 Prefs: state.Prefs 9819 }))(_Sections); 9820 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/Highlights/Highlights.jsx 9821 function Highlights_extends() { return Highlights_extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, Highlights_extends.apply(null, arguments); } 9822 /* This Source Code Form is subject to the terms of the Mozilla Public 9823 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 9824 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 9825 9826 9827 9828 9829 class _Highlights extends (external_React_default()).PureComponent { 9830 render() { 9831 const section = this.props.Sections.find(s => s.id === "highlights"); 9832 if (!section || !section.enabled) { 9833 return null; 9834 } 9835 return /*#__PURE__*/external_React_default().createElement("div", { 9836 className: "ds-highlights sections-list" 9837 }, /*#__PURE__*/external_React_default().createElement(SectionIntl, Highlights_extends({}, section, { 9838 isFixed: true 9839 }))); 9840 } 9841 } 9842 const Highlights = (0,external_ReactRedux_namespaceObject.connect)(state => ({ 9843 Sections: state.Sections 9844 }))(_Highlights); 9845 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/HorizontalRule/HorizontalRule.jsx 9846 /* This Source Code Form is subject to the terms of the Mozilla Public 9847 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 9848 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 9849 9850 9851 class HorizontalRule extends (external_React_default()).PureComponent { 9852 render() { 9853 return /*#__PURE__*/external_React_default().createElement("hr", { 9854 className: "ds-hr" 9855 }); 9856 } 9857 } 9858 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx 9859 /* This Source Code Form is subject to the terms of the Mozilla Public 9860 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 9861 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 9862 9863 9864 9865 9866 9867 class Topic extends (external_React_default()).PureComponent { 9868 constructor(props) { 9869 super(props); 9870 this.onLinkClick = this.onLinkClick.bind(this); 9871 } 9872 onLinkClick(event) { 9873 if (this.props.dispatch) { 9874 this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({ 9875 event: "CLICK", 9876 source: "POPULAR_TOPICS", 9877 action_position: 0, 9878 value: { 9879 topic: event.target.text.toLowerCase().replace(` `, `-`) 9880 } 9881 })); 9882 } 9883 } 9884 render() { 9885 const { 9886 url, 9887 name: topicName 9888 } = this.props; 9889 return /*#__PURE__*/external_React_default().createElement(SafeAnchor, { 9890 onLinkClick: this.onLinkClick, 9891 className: this.props.className, 9892 url: url 9893 }, topicName); 9894 } 9895 } 9896 class Navigation extends (external_React_default()).PureComponent { 9897 render() { 9898 let links = this.props.links || []; 9899 const alignment = this.props.alignment || "centered"; 9900 const header = this.props.header || {}; 9901 const english = this.props.locale.startsWith("en-"); 9902 const privacyNotice = this.props.privacyNoticeURL || {}; 9903 const { 9904 newFooterSection 9905 } = this.props; 9906 const className = `ds-navigation ds-navigation-${alignment} ${newFooterSection ? `ds-navigation-new-topics` : ``}`; 9907 let { 9908 title 9909 } = header; 9910 if (newFooterSection) { 9911 title = { 9912 id: "newtab-pocket-new-topics-title" 9913 }; 9914 if (this.props.extraLinks) { 9915 links = [...links.slice(0, links.length - 1), ...this.props.extraLinks, links[links.length - 1]]; 9916 } 9917 } 9918 return /*#__PURE__*/external_React_default().createElement("div", { 9919 className: className 9920 }, title && english ? /*#__PURE__*/external_React_default().createElement(FluentOrText, { 9921 message: title 9922 }, /*#__PURE__*/external_React_default().createElement("span", { 9923 className: "ds-navigation-header" 9924 })) : null, english ? /*#__PURE__*/external_React_default().createElement("ul", null, links && links.map(t => /*#__PURE__*/external_React_default().createElement("li", { 9925 key: t.name 9926 }, /*#__PURE__*/external_React_default().createElement(Topic, { 9927 url: t.url, 9928 name: t.name, 9929 dispatch: this.props.dispatch 9930 })))) : null, !newFooterSection ? /*#__PURE__*/external_React_default().createElement(SafeAnchor, { 9931 className: "ds-navigation-privacy", 9932 url: privacyNotice.url 9933 }, /*#__PURE__*/external_React_default().createElement(FluentOrText, { 9934 message: privacyNotice.title 9935 })) : null, newFooterSection ? /*#__PURE__*/external_React_default().createElement("div", { 9936 className: "ds-navigation-family" 9937 }, /*#__PURE__*/external_React_default().createElement("span", { 9938 className: "icon firefox-logo" 9939 }), /*#__PURE__*/external_React_default().createElement("span", null, "|"), /*#__PURE__*/external_React_default().createElement("span", { 9940 className: "icon pocket-logo" 9941 }), /*#__PURE__*/external_React_default().createElement("span", { 9942 className: "ds-navigation-family-message", 9943 "data-l10n-id": "newtab-pocket-pocket-firefox-family" 9944 })) : null); 9945 } 9946 } 9947 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/PrivacyLink/PrivacyLink.jsx 9948 /* This Source Code Form is subject to the terms of the Mozilla Public 9949 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 9950 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 9951 9952 9953 9954 9955 class PrivacyLink extends (external_React_default()).PureComponent { 9956 render() { 9957 const { 9958 properties 9959 } = this.props; 9960 return /*#__PURE__*/external_React_default().createElement("div", { 9961 className: "ds-privacy-link" 9962 }, /*#__PURE__*/external_React_default().createElement(SafeAnchor, { 9963 url: properties.url 9964 }, /*#__PURE__*/external_React_default().createElement(FluentOrText, { 9965 message: properties.title 9966 }))); 9967 } 9968 } 9969 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/SectionTitle/SectionTitle.jsx 9970 /* This Source Code Form is subject to the terms of the Mozilla Public 9971 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 9972 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 9973 9974 9975 class SectionTitle extends (external_React_default()).PureComponent { 9976 render() { 9977 const { 9978 header: { 9979 title, 9980 subtitle 9981 } 9982 } = this.props; 9983 return /*#__PURE__*/external_React_default().createElement("div", { 9984 className: "ds-section-title" 9985 }, /*#__PURE__*/external_React_default().createElement("div", { 9986 className: "title" 9987 }, title), subtitle ? /*#__PURE__*/external_React_default().createElement("div", { 9988 className: "subtitle" 9989 }, subtitle) : null); 9990 } 9991 } 9992 ;// CONCATENATED MODULE: ./content-src/lib/selectLayoutRender.mjs 9993 /* This Source Code Form is subject to the terms of the Mozilla Public 9994 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 9995 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 9996 9997 const selectLayoutRender = ({ state = {}, prefs = {} }) => { 9998 const { layout, feeds, spocs } = state; 9999 let spocIndexPlacementMap = {}; 10000 10001 /* This function fills spoc positions on a per placement basis with available spocs. 10002 * It does this by looping through each position for a placement and replacing a rec with a spoc. 10003 * If it runs out of spocs or positions, it stops. 10004 * If it sees the same placement again, it remembers the previous spoc index, and continues. 10005 * If it sees a blocked spoc, it skips that position leaving in a regular story. 10006 */ 10007 function fillSpocPositionsForPlacement( 10008 data, 10009 spocsPositions, 10010 spocsData, 10011 placementName 10012 ) { 10013 if ( 10014 !spocIndexPlacementMap[placementName] && 10015 spocIndexPlacementMap[placementName] !== 0 10016 ) { 10017 spocIndexPlacementMap[placementName] = 0; 10018 } 10019 const results = [...data]; 10020 for (let position of spocsPositions) { 10021 const spoc = spocsData[spocIndexPlacementMap[placementName]]; 10022 // If there are no spocs left, we can stop filling positions. 10023 if (!spoc) { 10024 break; 10025 } 10026 10027 // A placement could be used in two sections. 10028 // In these cases, we want to maintain the index of the previous section. 10029 // If we didn't do this, it might duplicate spocs. 10030 spocIndexPlacementMap[placementName]++; 10031 10032 // A spoc that's blocked is removed from the source for subsequent newtab loads. 10033 // If we have a spoc in the source that's blocked, it means it was *just* blocked, 10034 // and in this case, we skip this position, and show a regular spoc instead. 10035 if (!spocs.blocked.includes(spoc.url)) { 10036 results.splice(position.index, 0, spoc); 10037 } 10038 } 10039 10040 return results; 10041 } 10042 10043 const positions = {}; 10044 const DS_COMPONENTS = [ 10045 "Message", 10046 "SectionTitle", 10047 "Navigation", 10048 "Widgets", 10049 "CardGrid", 10050 "HorizontalRule", 10051 "PrivacyLink", 10052 ]; 10053 10054 const filterArray = []; 10055 10056 // Filter sections is Topsites are turned off 10057 if (!prefs["feeds.topsites"]) { 10058 filterArray.push("TopSites"); 10059 } 10060 10061 // Filter sections is Widgets are turned off 10062 // Note extra logic is required bc this feature can be enabled via Nimbus 10063 const nimbusWidgetsTrainhopEnabled = prefs.trainhopConfig?.widgets?.enabled; 10064 const nimbusWidgetsEnabled = prefs.widgetsConfig?.enabled; 10065 const widgetsEnabled = prefs["widgets.system.enabled"]; 10066 if ( 10067 !nimbusWidgetsTrainhopEnabled && 10068 !nimbusWidgetsEnabled && 10069 !widgetsEnabled 10070 ) { 10071 filterArray.push("Widgets"); 10072 } 10073 10074 // Filter sections is Recommended Stories are turned off 10075 const pocketEnabled = 10076 prefs["feeds.section.topstories"] && prefs["feeds.system.topstories"]; 10077 if (!pocketEnabled) { 10078 filterArray.push( 10079 // Bug 1980459 - Do not remove Widgets if DS is disabled 10080 ...DS_COMPONENTS.filter(component => component !== "Widgets") 10081 ); 10082 } 10083 10084 // function to determine amount of tiles shown per section per viewport 10085 function getMaxTiles(responsiveLayouts) { 10086 return responsiveLayouts 10087 .flatMap(responsiveLayout => responsiveLayout) 10088 .reduce((acc, t) => { 10089 acc[t.columnCount] = t.tiles.length; 10090 10091 // Update maxTile if current tile count is greater 10092 if (!acc.maxTile || t.tiles.length > acc.maxTile) { 10093 acc.maxTile = t.tiles.length; 10094 } 10095 return acc; 10096 }, {}); 10097 } 10098 10099 const placeholderComponent = component => { 10100 if (!component.feed) { 10101 // TODO we now need a placeholder for topsites. 10102 return { 10103 ...component, 10104 data: { 10105 spocs: [], 10106 }, 10107 }; 10108 } 10109 const data = { 10110 recommendations: [], 10111 sections: [ 10112 { 10113 layout: { 10114 responsiveLayouts: [], 10115 }, 10116 data: [], 10117 }, 10118 ], 10119 }; 10120 10121 let items = 0; 10122 if (component.properties && component.properties.items) { 10123 items = component.properties.items; 10124 } 10125 for (let i = 0; i < items; i++) { 10126 data.recommendations.push({ placeholder: true }); 10127 } 10128 10129 const sectionsEnabled = prefs["discoverystream.sections.enabled"]; 10130 if (sectionsEnabled) { 10131 for (let i = 0; i < items; i++) { 10132 data.sections[0].data.push({ placeholder: true }); 10133 } 10134 } 10135 10136 return { ...component, data }; 10137 }; 10138 10139 // TODO update devtools to show placements 10140 const handleSpocs = (data = [], spocsPositions, spocsPlacement) => { 10141 let result = [...data]; 10142 // Do we ever expect to possibly have a spoc. 10143 if (spocsPositions?.length) { 10144 const placement = spocsPlacement || {}; 10145 const placementName = placement.name || "newtab_spocs"; 10146 const spocsData = spocs.data[placementName]; 10147 10148 // We expect a spoc, spocs are loaded, and the server returned spocs. 10149 if (spocs.loaded && spocsData?.items?.length) { 10150 // Since banner-type ads are placed by row and don't use the normal spoc position, 10151 // dont combine with content 10152 const excludedSpocs = ["billboard", "leaderboard"]; 10153 const filteredSpocs = spocsData?.items?.filter( 10154 item => !excludedSpocs.includes(item.format) 10155 ); 10156 result = fillSpocPositionsForPlacement( 10157 result, 10158 spocsPositions, 10159 filteredSpocs, 10160 placementName 10161 ); 10162 } 10163 } 10164 return result; 10165 }; 10166 10167 const handleSections = (sections = [], recommendations = []) => { 10168 let result = sections.sort((a, b) => a.receivedRank - b.receivedRank); 10169 10170 const sectionsMap = recommendations.reduce((acc, recommendation) => { 10171 const { section } = recommendation; 10172 acc[section] = acc[section] || []; 10173 acc[section].push(recommendation); 10174 return acc; 10175 }, {}); 10176 10177 result.forEach(section => { 10178 const { sectionKey } = section; 10179 section.data = sectionsMap[sectionKey]; 10180 }); 10181 10182 return result; 10183 }; 10184 10185 const handleComponent = component => { 10186 if (component?.spocs?.positions?.length) { 10187 const placement = component.placement || {}; 10188 const placementName = placement.name || "newtab_spocs"; 10189 const spocsData = spocs.data[placementName]; 10190 if (spocs.loaded && spocsData?.items?.length) { 10191 return { 10192 ...component, 10193 data: { 10194 spocs: spocsData.items 10195 .filter(spoc => spoc && !spocs.blocked.includes(spoc.url)) 10196 .map((spoc, index) => ({ 10197 ...spoc, 10198 pos: index, 10199 })), 10200 }, 10201 }; 10202 } 10203 } 10204 return { 10205 ...component, 10206 data: { 10207 spocs: [], 10208 }, 10209 }; 10210 }; 10211 10212 const handleComponentWithFeed = component => { 10213 positions[component.type] = positions[component.type] || 0; 10214 let data = { 10215 recommendations: [], 10216 sections: [], 10217 }; 10218 10219 const feed = feeds.data[component.feed.url]; 10220 if (feed?.data) { 10221 data = { 10222 ...feed.data, 10223 recommendations: [...(feed.data.recommendations || [])], 10224 sections: [...(feed.data.sections || [])], 10225 }; 10226 } 10227 10228 if (component && component.properties && component.properties.offset) { 10229 data = { 10230 ...data, 10231 recommendations: data.recommendations.slice( 10232 component.properties.offset 10233 ), 10234 }; 10235 } 10236 const spocsPositions = component?.spocs?.positions; 10237 const spocsPlacement = component?.placement; 10238 10239 const sectionsEnabled = prefs["discoverystream.sections.enabled"]; 10240 data = { 10241 ...data, 10242 ...(sectionsEnabled 10243 ? { 10244 sections: handleSections(data.sections, data.recommendations).map( 10245 section => { 10246 const sectionsSpocsPositions = []; 10247 section.layout.responsiveLayouts 10248 // Initial position for spocs is going to be for the smallest breakpoint. 10249 // We can then move it from there via breakpoints. 10250 .find(item => item.columnCount === 1) 10251 .tiles.forEach(tile => { 10252 if (tile.hasAd) { 10253 sectionsSpocsPositions.push({ index: tile.position }); 10254 } 10255 }); 10256 return { 10257 ...section, 10258 data: handleSpocs( 10259 section.data, 10260 sectionsSpocsPositions, 10261 spocsPlacement 10262 ), 10263 }; 10264 } 10265 ), 10266 // We don't fill spocs in recs if sections are enabled, 10267 // because recs are not going to be seen. 10268 recommendations: data.recommendations, 10269 } 10270 : { 10271 recommendations: handleSpocs( 10272 data.recommendations, 10273 spocsPositions, 10274 spocsPlacement 10275 ), 10276 }), 10277 }; 10278 10279 let items = 0; 10280 if (component.properties && component.properties.items) { 10281 items = Math.min(component.properties.items, data.recommendations.length); 10282 } 10283 10284 // loop through a component items 10285 // Store the items position sequentially for multiple components of the same type. 10286 // Example: A second card grid starts pos offset from the last card grid. 10287 for (let i = 0; i < items; i++) { 10288 data.recommendations[i] = { 10289 ...data.recommendations[i], 10290 pos: positions[component.type]++, 10291 }; 10292 } 10293 10294 // Setup absolute positions for sections layout. 10295 if (sectionsEnabled) { 10296 let currentPosition = 0; 10297 data.sections.forEach(section => { 10298 // We assume the count for the breakpoint with the most tiles. 10299 const { maxTile } = getMaxTiles(section?.layout?.responsiveLayouts); 10300 for (let i = 0; i < maxTile; i++) { 10301 if (section.data[i]) { 10302 section.data[i] = { 10303 ...section.data[i], 10304 pos: currentPosition++, 10305 }; 10306 } 10307 } 10308 }); 10309 } 10310 10311 return { ...component, data }; 10312 }; 10313 10314 const renderLayout = () => { 10315 const renderedLayoutArray = []; 10316 for (const row of layout.filter( 10317 r => r.components.filter(c => !filterArray.includes(c.type)).length 10318 )) { 10319 let components = []; 10320 renderedLayoutArray.push({ 10321 ...row, 10322 components, 10323 }); 10324 for (const component of row.components.filter( 10325 c => !filterArray.includes(c.type) 10326 )) { 10327 const spocsConfig = component.spocs; 10328 if (spocsConfig || component.feed) { 10329 if ( 10330 (component.feed && !feeds.data[component.feed.url]) || 10331 (spocsConfig && 10332 spocsConfig.positions && 10333 spocsConfig.positions.length && 10334 !spocs.loaded) 10335 ) { 10336 components.push(placeholderComponent(component)); 10337 } else if (component.feed) { 10338 components.push(handleComponentWithFeed(component)); 10339 } else { 10340 components.push(handleComponent(component)); 10341 } 10342 } else { 10343 components.push(component); 10344 } 10345 } 10346 } 10347 return renderedLayoutArray; 10348 }; 10349 10350 const layoutRender = renderLayout(); 10351 10352 return { layoutRender }; 10353 }; 10354 10355 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/SectionContextMenu/SectionContextMenu.jsx 10356 /* This Source Code Form is subject to the terms of the Mozilla Public 10357 * License, v. 2.0. If a copy of the MPL was not distributed with this 10358 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 10359 10360 10361 10362 10363 /** 10364 * A context menu for blocking, following and unfollowing sections. 10365 * 10366 * @param props 10367 * @returns {React.FunctionComponent} 10368 */ 10369 function SectionContextMenu({ 10370 type = "DISCOVERY_STREAM", 10371 title, 10372 source, 10373 index, 10374 dispatch, 10375 sectionKey, 10376 following, 10377 sectionPersonalization, 10378 sectionPosition 10379 }) { 10380 // Initial context menu options: block this section only. 10381 const SECTIONS_CONTEXT_MENU_OPTIONS = ["SectionBlock"]; 10382 const [showContextMenu, setShowContextMenu] = (0,external_React_namespaceObject.useState)(false); 10383 if (following) { 10384 SECTIONS_CONTEXT_MENU_OPTIONS.push("SectionUnfollow"); 10385 } 10386 const onClick = e => { 10387 e.preventDefault(); 10388 setShowContextMenu(!showContextMenu); 10389 }; 10390 const onUpdate = () => { 10391 setShowContextMenu(!showContextMenu); 10392 }; 10393 return /*#__PURE__*/external_React_default().createElement("div", { 10394 className: "section-context-menu" 10395 }, /*#__PURE__*/external_React_default().createElement("moz-button", { 10396 type: "icon", 10397 size: "default", 10398 iconsrc: "chrome://global/skin/icons/more.svg", 10399 title: title || source, 10400 onClick: onClick 10401 }), showContextMenu && /*#__PURE__*/external_React_default().createElement(LinkMenu, { 10402 onUpdate: onUpdate, 10403 dispatch: dispatch, 10404 index: index, 10405 source: type.toUpperCase(), 10406 options: SECTIONS_CONTEXT_MENU_OPTIONS, 10407 shouldSendImpressionStats: true, 10408 site: { 10409 sectionPersonalization, 10410 sectionKey, 10411 sectionPosition, 10412 title 10413 } 10414 })); 10415 } 10416 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/InterestPicker/InterestPicker.jsx 10417 /* This Source Code Form is subject to the terms of the Mozilla Public 10418 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 10419 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 10420 10421 10422 10423 10424 10425 const PREF_VISIBLE_SECTIONS = "discoverystream.sections.interestPicker.visibleSections"; 10426 10427 /** 10428 * Shows a list of recommended topics with visual indication whether 10429 * the user follows some of the topics (active, blue, selected topics) 10430 * or is yet to do so (neutrally-coloured topics with a "plus" button). 10431 * 10432 * @returns {React.Element} 10433 */ 10434 function InterestPicker({ 10435 title, 10436 subtitle, 10437 interests, 10438 receivedFeedRank 10439 }) { 10440 const dispatch = (0,external_ReactRedux_namespaceObject.useDispatch)(); 10441 const focusedRef = (0,external_React_namespaceObject.useRef)(null); 10442 const focusRef = (0,external_React_namespaceObject.useRef)(null); 10443 const [focusedIndex, setFocusedIndex] = (0,external_React_namespaceObject.useState)(0); 10444 const prefs = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Prefs.values); 10445 const { 10446 sectionPersonalization 10447 } = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.DiscoveryStream); 10448 const visibleSections = prefs[PREF_VISIBLE_SECTIONS]?.split(",").map(item => item.trim()).filter(item => item); 10449 const handleIntersection = (0,external_React_namespaceObject.useCallback)(() => { 10450 dispatch(actionCreators.AlsoToMain({ 10451 type: actionTypes.INLINE_SELECTION_IMPRESSION, 10452 data: { 10453 section_position: receivedFeedRank 10454 } 10455 })); 10456 }, [dispatch, receivedFeedRank]); 10457 const ref = useIntersectionObserver(handleIntersection); 10458 const onKeyDown = (0,external_React_namespaceObject.useCallback)(e => { 10459 if (e.key === "ArrowDown" || e.key === "ArrowUp") { 10460 // prevent the page from scrolling up/down while navigating. 10461 e.preventDefault(); 10462 } 10463 if (focusedRef.current?.nextSibling?.querySelector("input") && e.key === "ArrowDown") { 10464 focusedRef.current.nextSibling.querySelector("input").tabIndex = 0; 10465 focusedRef.current.nextSibling.querySelector("input").focus(); 10466 } 10467 if (focusedRef.current?.previousSibling?.querySelector("input") && e.key === "ArrowUp") { 10468 focusedRef.current.previousSibling.querySelector("input").tabIndex = 0; 10469 focusedRef.current.previousSibling.querySelector("input").focus(); 10470 } 10471 }, []); 10472 function onWrapperFocus() { 10473 focusRef.current?.addEventListener("keydown", onKeyDown); 10474 } 10475 function onWrapperBlur() { 10476 focusRef.current?.removeEventListener("keydown", onKeyDown); 10477 } 10478 function onItemFocus(index) { 10479 setFocusedIndex(index); 10480 } 10481 10482 // Updates user preferences as they follow or unfollow topics 10483 // by selecting them from the list 10484 function handleChange(e, index) { 10485 const { 10486 name: topic, 10487 checked 10488 } = e.target; 10489 let updatedSections = { 10490 ...sectionPersonalization 10491 }; 10492 if (checked) { 10493 updatedSections[topic] = { 10494 isFollowed: true, 10495 isBlocked: false, 10496 followedAt: new Date().toISOString() 10497 }; 10498 if (!visibleSections.includes(topic)) { 10499 // add section to visible sections and place after the inline picker 10500 // subtract 1 from the rank so that it is normalized with array index 10501 visibleSections.splice(receivedFeedRank - 1, 0, topic); 10502 dispatch(actionCreators.SetPref(PREF_VISIBLE_SECTIONS, visibleSections.join(", "))); 10503 } 10504 } else { 10505 delete updatedSections[topic]; 10506 } 10507 dispatch(actionCreators.OnlyToMain({ 10508 type: actionTypes.INLINE_SELECTION_CLICK, 10509 data: { 10510 topic, 10511 is_followed: checked, 10512 topic_position: index, 10513 section_position: receivedFeedRank 10514 } 10515 })); 10516 dispatch(actionCreators.AlsoToMain({ 10517 type: actionTypes.SECTION_PERSONALIZATION_SET, 10518 data: updatedSections 10519 })); 10520 } 10521 return /*#__PURE__*/external_React_default().createElement("section", { 10522 className: "inline-selection-wrapper ds-section", 10523 ref: el => { 10524 ref.current = [el]; 10525 } 10526 }, /*#__PURE__*/external_React_default().createElement("div", { 10527 className: "section-heading" 10528 }, /*#__PURE__*/external_React_default().createElement("div", { 10529 className: "section-title-wrapper" 10530 }, /*#__PURE__*/external_React_default().createElement("h2", { 10531 className: "section-title" 10532 }, title), /*#__PURE__*/external_React_default().createElement("p", { 10533 className: "section-subtitle" 10534 }, subtitle))), /*#__PURE__*/external_React_default().createElement("ul", { 10535 className: "topic-list", 10536 onFocus: onWrapperFocus, 10537 onBlur: onWrapperBlur, 10538 ref: focusRef 10539 }, interests.map((interest, index) => { 10540 const checked = sectionPersonalization[interest.sectionId]?.isFollowed; 10541 return /*#__PURE__*/external_React_default().createElement("li", { 10542 key: interest.sectionId, 10543 ref: index === focusedIndex ? focusedRef : null 10544 }, /*#__PURE__*/external_React_default().createElement("label", null, /*#__PURE__*/external_React_default().createElement("input", { 10545 type: "checkbox", 10546 id: interest.sectionId, 10547 name: interest.sectionId, 10548 checked: checked, 10549 "aria-checked": checked, 10550 onChange: e => handleChange(e, index), 10551 key: `${interest.sectionId}-${checked}` // Force remount to sync DOM state with React state 10552 , 10553 tabIndex: index === focusedIndex ? 0 : -1, 10554 onFocus: () => { 10555 onItemFocus(index); 10556 } 10557 }), /*#__PURE__*/external_React_default().createElement("span", { 10558 className: "topic-item-label" 10559 }, interest.title || ""), /*#__PURE__*/external_React_default().createElement("div", { 10560 className: `topic-item-icon icon ${checked ? "icon-check-filled" : "icon-add-circle-fill"}` 10561 }))); 10562 })), /*#__PURE__*/external_React_default().createElement("p", { 10563 className: "learn-more-copy" 10564 }, /*#__PURE__*/external_React_default().createElement("a", { 10565 href: prefs["support.url"], 10566 "data-l10n-id": "newtab-topic-selection-privacy-link" 10567 }))); 10568 } 10569 10570 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/PersonalizedCard/PersonalizedCard.jsx 10571 /* This Source Code Form is subject to the terms of the Mozilla Public 10572 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 10573 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 10574 10575 10576 10577 10578 const PersonalizedCard = ({ 10579 dispatch, 10580 handleDismiss, 10581 handleClick, 10582 handleBlock, 10583 messageData 10584 }) => { 10585 const kitFox = "chrome://newtab/content/data/content/assets/kit.png"; 10586 const onDismiss = (0,external_React_namespaceObject.useCallback)(() => { 10587 handleDismiss(); 10588 handleBlock(); 10589 }, [handleDismiss, handleBlock]); 10590 const onToggleClick = (0,external_React_namespaceObject.useCallback)(elementId => { 10591 dispatch({ 10592 type: actionTypes.SHOW_PERSONALIZE 10593 }); 10594 dispatch(actionCreators.UserEvent({ 10595 event: "SHOW_PERSONALIZE" 10596 })); 10597 handleClick(elementId); 10598 }, [dispatch, handleClick]); 10599 return /*#__PURE__*/external_React_default().createElement("aside", { 10600 className: "personalized-card-wrapper" 10601 }, /*#__PURE__*/external_React_default().createElement("div", { 10602 className: "personalized-card-dismiss" 10603 }, /*#__PURE__*/external_React_default().createElement("moz-button", { 10604 type: "icon ghost", 10605 iconSrc: "chrome://global/skin/icons/close.svg", 10606 onClick: onDismiss, 10607 "data-l10n-id": "newtab-toast-dismiss-button" 10608 })), /*#__PURE__*/external_React_default().createElement("div", { 10609 className: "personalized-card-inner" 10610 }, /*#__PURE__*/external_React_default().createElement("img", { 10611 src: kitFox, 10612 alt: "" 10613 }), /*#__PURE__*/external_React_default().createElement("h2", null, messageData.content.cardTitle), /*#__PURE__*/external_React_default().createElement("p", null, messageData.content.cardMessage), /*#__PURE__*/external_React_default().createElement("div", { 10614 className: "personalized-card-cta-wrapper" 10615 }, /*#__PURE__*/external_React_default().createElement("moz-button", { 10616 type: "primary", 10617 class: "personalized-card-cta", 10618 onClick: () => onToggleClick("open-personalization-panel") 10619 }, messageData.content.ctaText), /*#__PURE__*/external_React_default().createElement(SafeAnchor, { 10620 className: "personalized-card-link", 10621 dispatch: dispatch, 10622 url: messageData.content.linkUrl || "https://support.mozilla.org/", 10623 onLinkClick: () => { 10624 handleClick("link-click"); 10625 } 10626 }, messageData.content.linkText)))); 10627 }; 10628 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/FeatureHighlight/FollowSectionButtonHighlight.jsx 10629 /* This Source Code Form is subject to the terms of the Mozilla Public 10630 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 10631 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 10632 10633 10634 10635 function FollowSectionButtonHighlight({ 10636 arrowPosition, 10637 dispatch, 10638 feature, 10639 handleBlock, 10640 handleDismiss, 10641 messageData, 10642 position, 10643 verticalPosition 10644 }) { 10645 const onDismiss = (0,external_React_namespaceObject.useCallback)(() => { 10646 handleDismiss(); 10647 handleBlock(); 10648 }, [handleDismiss, handleBlock]); 10649 return /*#__PURE__*/external_React_default().createElement("div", { 10650 className: `follow-section-button-highlight ${messageData.content?.darkModeDismiss ? "is-inverted-dark-dismiss-button" : ""}` 10651 }, /*#__PURE__*/external_React_default().createElement(FeatureHighlight, { 10652 position: position, 10653 arrowPosition: arrowPosition, 10654 verticalPosition: verticalPosition, 10655 feature: feature, 10656 dispatch: dispatch, 10657 message: /*#__PURE__*/external_React_default().createElement("div", { 10658 className: "follow-section-button-highlight-content" 10659 }, /*#__PURE__*/external_React_default().createElement("picture", { 10660 className: "follow-section-button-highlight-image" 10661 }, /*#__PURE__*/external_React_default().createElement("source", { 10662 srcSet: messageData.content?.darkModeImageURL || "chrome://newtab/content/data/content/assets/highlights/omc-newtab-follow.svg", 10663 media: "(prefers-color-scheme: dark)" 10664 }), /*#__PURE__*/external_React_default().createElement("source", { 10665 srcSet: messageData.content?.imageURL || "chrome://newtab/content/data/content/assets/highlights/omc-newtab-follow.svg", 10666 media: "(prefers-color-scheme: light)" 10667 }), /*#__PURE__*/external_React_default().createElement("img", { 10668 width: "320", 10669 height: "195", 10670 alt: "" 10671 })), /*#__PURE__*/external_React_default().createElement("div", { 10672 className: "follow-section-button-highlight-copy" 10673 }, messageData.content?.cardTitle ? /*#__PURE__*/external_React_default().createElement("p", { 10674 className: "title" 10675 }, messageData.content.cardTitle) : /*#__PURE__*/external_React_default().createElement("p", { 10676 className: "title", 10677 "data-l10n-id": "newtab-section-follow-highlight-title" 10678 }), messageData.content?.cardMessage ? /*#__PURE__*/external_React_default().createElement("p", { 10679 className: "subtitle" 10680 }, messageData.content.cardMessage) : /*#__PURE__*/external_React_default().createElement("p", { 10681 className: "subtitle", 10682 "data-l10n-id": "newtab-section-follow-highlight-subtitle" 10683 }))), 10684 openedOverride: true, 10685 showButtonIcon: false, 10686 dismissCallback: onDismiss, 10687 outsideClickCallback: handleDismiss 10688 })); 10689 } 10690 ;// CONCATENATED MODULE: ./content-src/components/Weather/LocationSearch.jsx 10691 /* This Source Code Form is subject to the terms of the Mozilla Public 10692 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 10693 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 10694 10695 10696 10697 10698 function LocationSearch({ 10699 outerClassName 10700 }) { 10701 // should be the location object from suggestedLocations 10702 const [selectedLocation, setSelectedLocation] = (0,external_React_namespaceObject.useState)(""); 10703 const suggestedLocations = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Weather.suggestedLocations); 10704 const locationSearchString = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Weather.locationSearchString); 10705 const [userInput, setUserInput] = (0,external_React_namespaceObject.useState)(locationSearchString || ""); 10706 const inputRef = (0,external_React_namespaceObject.useRef)(null); 10707 const dispatch = (0,external_ReactRedux_namespaceObject.useDispatch)(); 10708 (0,external_React_namespaceObject.useEffect)(() => { 10709 if (selectedLocation) { 10710 dispatch(actionCreators.AlsoToMain({ 10711 type: actionTypes.WEATHER_LOCATION_DATA_UPDATE, 10712 data: { 10713 city: selectedLocation.localized_name, 10714 adminName: selectedLocation.administrative_area, 10715 country: selectedLocation.country 10716 } 10717 })); 10718 dispatch(actionCreators.SetPref("weather.query", selectedLocation.key)); 10719 dispatch(actionCreators.BroadcastToContent({ 10720 type: actionTypes.WEATHER_SEARCH_ACTIVE, 10721 data: false 10722 })); 10723 } 10724 }, [selectedLocation, dispatch]); 10725 10726 // when component mounts, set focus to input 10727 (0,external_React_namespaceObject.useEffect)(() => { 10728 inputRef?.current?.focus(); 10729 }, [inputRef]); 10730 function handleChange(event) { 10731 const { 10732 value 10733 } = event.target; 10734 setUserInput(value); 10735 10736 // if the user input contains less than three characters and suggestedLocations is not an empty array, 10737 // reset suggestedLocations to [] so there aren't incorrect items in the datalist 10738 if (value.length < 3 && suggestedLocations.length) { 10739 dispatch(actionCreators.AlsoToMain({ 10740 type: actionTypes.WEATHER_LOCATION_SUGGESTIONS_UPDATE, 10741 data: [] 10742 })); 10743 } 10744 // find match in suggestedLocation array 10745 const match = suggestedLocations?.find(({ 10746 key 10747 }) => key === value); 10748 if (match) { 10749 setSelectedLocation(match); 10750 setUserInput(`${match.localized_name}, ${match.administrative_area.localized_name}`); 10751 } else if (value.length >= 3 && !match) { 10752 dispatch(actionCreators.AlsoToMain({ 10753 type: actionTypes.WEATHER_LOCATION_SEARCH_UPDATE, 10754 data: value 10755 })); 10756 } 10757 } 10758 function handleCloseSearch() { 10759 dispatch(actionCreators.BroadcastToContent({ 10760 type: actionTypes.WEATHER_SEARCH_ACTIVE, 10761 data: false 10762 })); 10763 setUserInput(""); 10764 } 10765 function handleKeyDown(e) { 10766 if (e.key === "Escape") { 10767 handleCloseSearch(); 10768 } 10769 } 10770 return /*#__PURE__*/external_React_default().createElement("div", { 10771 className: `${outerClassName} location-search` 10772 }, /*#__PURE__*/external_React_default().createElement("div", { 10773 className: "location-input-wrapper" 10774 }, /*#__PURE__*/external_React_default().createElement("div", { 10775 className: "search-icon" 10776 }), /*#__PURE__*/external_React_default().createElement("input", { 10777 ref: inputRef, 10778 list: "merino-location-list", 10779 type: "text", 10780 "data-l10n-id": "newtab-weather-change-location-search-input-placeholder", 10781 onChange: handleChange, 10782 value: userInput, 10783 onKeyDown: handleKeyDown 10784 }), /*#__PURE__*/external_React_default().createElement("moz-button", { 10785 class: "close-icon", 10786 type: "icon ghost", 10787 size: "small", 10788 iconSrc: "chrome://global/skin/icons/close.svg", 10789 onClick: handleCloseSearch 10790 }), /*#__PURE__*/external_React_default().createElement("datalist", { 10791 id: "merino-location-list" 10792 }, (suggestedLocations || []).map(merinoLocation => /*#__PURE__*/external_React_default().createElement("option", { 10793 value: merinoLocation.key, 10794 key: merinoLocation.key 10795 }, merinoLocation.localized_name, ",", " ", merinoLocation.administrative_area.localized_name))))); 10796 } 10797 10798 ;// CONCATENATED MODULE: ./content-src/components/Weather/Weather.jsx 10799 /* This Source Code Form is subject to the terms of the Mozilla Public 10800 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 10801 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 10802 10803 10804 10805 10806 10807 10808 10809 const Weather_VISIBLE = "visible"; 10810 const Weather_VISIBILITY_CHANGE_EVENT = "visibilitychange"; 10811 const PREF_SYSTEM_SHOW_WEATHER = "system.showWeather"; 10812 function WeatherPlaceholder() { 10813 const [isSeen, setIsSeen] = (0,external_React_namespaceObject.useState)(false); 10814 10815 // We are setting up a visibility and intersection event 10816 // so animations don't happen with headless automation. 10817 // The animations causes tests to fail beause they never stop, 10818 // and many tests wait until everything has stopped before passing. 10819 const ref = useIntersectionObserver(() => setIsSeen(true), 1); 10820 const isSeenClassName = isSeen ? `placeholder-seen` : ``; 10821 return /*#__PURE__*/external_React_default().createElement("div", { 10822 className: `weather weather-placeholder ${isSeenClassName}`, 10823 ref: el => { 10824 ref.current = [el]; 10825 } 10826 }, /*#__PURE__*/external_React_default().createElement("div", { 10827 className: "placeholder-image placeholder-fill" 10828 }), /*#__PURE__*/external_React_default().createElement("div", { 10829 className: "placeholder-context" 10830 }, /*#__PURE__*/external_React_default().createElement("div", { 10831 className: "placeholder-header placeholder-fill" 10832 }), /*#__PURE__*/external_React_default().createElement("div", { 10833 className: "placeholder-description placeholder-fill" 10834 }))); 10835 } 10836 class _Weather extends (external_React_default()).PureComponent { 10837 constructor(props) { 10838 super(props); 10839 this.state = { 10840 contextMenuKeyboard: false, 10841 showContextMenu: false, 10842 url: "https://example.com", 10843 impressionSeen: false, 10844 errorSeen: false 10845 }; 10846 this.setImpressionRef = element => { 10847 this.impressionElement = element; 10848 }; 10849 this.setErrorRef = element => { 10850 this.errorElement = element; 10851 }; 10852 this.onClick = this.onClick.bind(this); 10853 this.onKeyDown = this.onKeyDown.bind(this); 10854 this.onUpdate = this.onUpdate.bind(this); 10855 this.onProviderClick = this.onProviderClick.bind(this); 10856 } 10857 componentDidMount() { 10858 const { 10859 props 10860 } = this; 10861 if (!props.dispatch) { 10862 return; 10863 } 10864 if (props.document.visibilityState === Weather_VISIBLE) { 10865 // Setup the impression observer once the page is visible. 10866 this.setImpressionObservers(); 10867 } else { 10868 // We should only ever send the latest impression stats ping, so remove any 10869 // older listeners. 10870 if (this._onVisibilityChange) { 10871 props.document.removeEventListener(Weather_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 10872 } 10873 this._onVisibilityChange = () => { 10874 if (props.document.visibilityState === Weather_VISIBLE) { 10875 // Setup the impression observer once the page is visible. 10876 this.setImpressionObservers(); 10877 props.document.removeEventListener(Weather_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 10878 } 10879 }; 10880 props.document.addEventListener(Weather_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 10881 } 10882 } 10883 componentWillUnmount() { 10884 // Remove observers on unmount 10885 if (this.observer && this.impressionElement) { 10886 this.observer.unobserve(this.impressionElement); 10887 } 10888 if (this.observer && this.errorElement) { 10889 this.observer.unobserve(this.errorElement); 10890 } 10891 if (this._onVisibilityChange) { 10892 this.props.document.removeEventListener(Weather_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 10893 } 10894 } 10895 setImpressionObservers() { 10896 if (this.impressionElement) { 10897 this.observer = new IntersectionObserver(this.onImpression.bind(this)); 10898 this.observer.observe(this.impressionElement); 10899 } 10900 if (this.errorElement) { 10901 this.observer = new IntersectionObserver(this.onError.bind(this)); 10902 this.observer.observe(this.errorElement); 10903 } 10904 } 10905 onImpression(entries) { 10906 if (this.state) { 10907 const entry = entries.find(e => e.isIntersecting); 10908 if (entry) { 10909 if (this.impressionElement) { 10910 this.observer.unobserve(this.impressionElement); 10911 } 10912 this.props.dispatch(actionCreators.OnlyToMain({ 10913 type: actionTypes.WEATHER_IMPRESSION 10914 })); 10915 10916 // Stop observing since element has been seen 10917 this.setState({ 10918 impressionSeen: true 10919 }); 10920 } 10921 } 10922 } 10923 onError(entries) { 10924 if (this.state) { 10925 const entry = entries.find(e => e.isIntersecting); 10926 if (entry) { 10927 if (this.errorElement) { 10928 this.observer.unobserve(this.errorElement); 10929 } 10930 this.props.dispatch(actionCreators.OnlyToMain({ 10931 type: actionTypes.WEATHER_LOAD_ERROR 10932 })); 10933 10934 // Stop observing since element has been seen 10935 this.setState({ 10936 errorSeen: true 10937 }); 10938 } 10939 } 10940 } 10941 openContextMenu(isKeyBoard) { 10942 if (this.props.onUpdate) { 10943 this.props.onUpdate(true); 10944 } 10945 this.setState({ 10946 showContextMenu: true, 10947 contextMenuKeyboard: isKeyBoard 10948 }); 10949 } 10950 onClick(event) { 10951 event.preventDefault(); 10952 this.openContextMenu(false, event); 10953 } 10954 onKeyDown(event) { 10955 if (event.key === "Enter" || event.key === " ") { 10956 event.preventDefault(); 10957 this.openContextMenu(true, event); 10958 } 10959 } 10960 onUpdate(showContextMenu) { 10961 if (this.props.onUpdate) { 10962 this.props.onUpdate(showContextMenu); 10963 } 10964 this.setState({ 10965 showContextMenu 10966 }); 10967 } 10968 onProviderClick() { 10969 this.props.dispatch(actionCreators.OnlyToMain({ 10970 type: actionTypes.WEATHER_OPEN_PROVIDER_URL, 10971 data: { 10972 source: "WEATHER" 10973 } 10974 })); 10975 } 10976 handleRejectOptIn = () => { 10977 (0,external_ReactRedux_namespaceObject.batch)(() => { 10978 this.props.dispatch(actionCreators.SetPref("weather.optInAccepted", false)); 10979 this.props.dispatch(actionCreators.SetPref("weather.optInDisplayed", false)); 10980 this.props.dispatch(actionCreators.AlsoToMain({ 10981 type: actionTypes.WEATHER_OPT_IN_PROMPT_SELECTION, 10982 data: "rejected opt-in" 10983 })); 10984 }); 10985 }; 10986 handleAcceptOptIn = () => { 10987 (0,external_ReactRedux_namespaceObject.batch)(() => { 10988 this.props.dispatch(actionCreators.AlsoToMain({ 10989 type: actionTypes.WEATHER_USER_OPT_IN_LOCATION 10990 })); 10991 this.props.dispatch(actionCreators.AlsoToMain({ 10992 type: actionTypes.WEATHER_OPT_IN_PROMPT_SELECTION, 10993 data: "accepted opt-in" 10994 })); 10995 }); 10996 }; 10997 isEnabled() { 10998 const { 10999 values 11000 } = this.props.Prefs; 11001 const systemValue = values[PREF_SYSTEM_SHOW_WEATHER] && values["feeds.weatherfeed"]; 11002 const experimentValue = values.trainhopConfig?.weather?.enabled; 11003 return systemValue || experimentValue; 11004 } 11005 render() { 11006 // Check if weather should be rendered 11007 if (!this.isEnabled()) { 11008 return false; 11009 } 11010 if (this.props.App.isForStartupCache.Weather || !this.props.Weather.initialized) { 11011 return /*#__PURE__*/external_React_default().createElement(WeatherPlaceholder, null); 11012 } 11013 const { 11014 showContextMenu 11015 } = this.state; 11016 const { 11017 props 11018 } = this; 11019 const { 11020 dispatch, 11021 Prefs, 11022 Weather 11023 } = props; 11024 const WEATHER_SUGGESTION = Weather.suggestions?.[0]; 11025 const outerClassName = ["weather", Weather.searchActive && "search", props.isInSection && "section-weather"].filter(v => v).join(" "); 11026 const showDetailedView = Prefs.values["weather.display"] === "detailed"; 11027 const weatherOptIn = Prefs.values["system.showWeatherOptIn"]; 11028 const nimbusWeatherOptInEnabled = Prefs.values.trainhopConfig?.weather?.weatherOptInEnabled; 11029 // Bug 2009484: Controls button order in opt-in dialog for A/B testing. 11030 // When true, "Not now" gets slot="primary"; 11031 // when false/undefined, "Yes" gets slot="primary". 11032 // Also note the primary button's position varies by platform: 11033 // on Windows, it appears on the left, 11034 // while on Linux and macOS, it appears on the right. 11035 const reverseOptInButtons = Prefs.values.trainhopConfig?.weather?.reverseOptInButtons; 11036 const optInDisplayed = Prefs.values["weather.optInDisplayed"]; 11037 const optInUserChoice = Prefs.values["weather.optInAccepted"]; 11038 const staticWeather = Prefs.values["weather.staticData.enabled"]; 11039 11040 // Conditionals for rendering feature based on prefs + nimbus experiment variables 11041 const isOptInEnabled = weatherOptIn || nimbusWeatherOptInEnabled; 11042 11043 // Opt-in dialog should only show if: 11044 // - weather enabled on customization menu 11045 // - weather opt-in pref is enabled 11046 // - opt-in prompt is enabled 11047 // - user hasn't accepted the opt-in yet 11048 const shouldShowOptInDialog = isOptInEnabled && optInDisplayed && !optInUserChoice; 11049 11050 // Show static weather data only if: 11051 // - weather is enabled on customization menu 11052 // - weather opt-in pref is enabled 11053 // - static weather data is enabled 11054 const showStaticData = isOptInEnabled && staticWeather; 11055 11056 // Note: The temperature units/display options will become secondary menu items 11057 const WEATHER_SOURCE_CONTEXT_MENU_OPTIONS = [...(Prefs.values["weather.locationSearchEnabled"] ? ["ChangeWeatherLocation"] : []), ...(isOptInEnabled ? ["DetectLocation"] : []), ...(Prefs.values["weather.temperatureUnits"] === "f" ? ["ChangeTempUnitCelsius"] : ["ChangeTempUnitFahrenheit"]), ...(Prefs.values["weather.display"] === "simple" ? ["ChangeWeatherDisplayDetailed"] : ["ChangeWeatherDisplaySimple"]), "HideWeather", "OpenLearnMoreURL"]; 11058 const WEATHER_SOURCE_SHORTENED_CONTEXT_MENU_OPTIONS = [...(Prefs.values["weather.locationSearchEnabled"] ? ["ChangeWeatherLocation"] : []), ...(isOptInEnabled ? ["DetectLocation"] : []), "HideWeather", "OpenLearnMoreURL"]; 11059 const contextMenu = contextOpts => /*#__PURE__*/external_React_default().createElement("div", { 11060 className: "weatherButtonContextMenuWrapper" 11061 }, /*#__PURE__*/external_React_default().createElement("button", { 11062 "aria-haspopup": "true", 11063 onKeyDown: this.onKeyDown, 11064 onClick: this.onClick, 11065 "data-l10n-id": "newtab-menu-section-tooltip", 11066 className: "weatherButtonContextMenu" 11067 }, showContextMenu ? /*#__PURE__*/external_React_default().createElement(LinkMenu, { 11068 dispatch: dispatch, 11069 index: 0, 11070 source: "WEATHER", 11071 onUpdate: this.onUpdate, 11072 options: contextOpts, 11073 site: { 11074 url: "https://support.mozilla.org/kb/customize-items-on-firefox-new-tab-page" 11075 }, 11076 link: "https://support.mozilla.org/kb/customize-items-on-firefox-new-tab-page", 11077 shouldSendImpressionStats: false 11078 }) : null)); 11079 if (Weather.searchActive) { 11080 return /*#__PURE__*/external_React_default().createElement(LocationSearch, { 11081 outerClassName: outerClassName 11082 }); 11083 } else if (WEATHER_SUGGESTION) { 11084 return /*#__PURE__*/external_React_default().createElement("div", { 11085 ref: this.setImpressionRef, 11086 className: outerClassName 11087 }, /*#__PURE__*/external_React_default().createElement("div", { 11088 className: "weatherCard" 11089 }, showStaticData ? /*#__PURE__*/external_React_default().createElement("div", { 11090 className: "weatherInfoLink staticWeatherInfo" 11091 }, /*#__PURE__*/external_React_default().createElement("div", { 11092 className: "weatherIconCol" 11093 }, /*#__PURE__*/external_React_default().createElement("span", { 11094 className: "weatherIcon iconId3" 11095 })), /*#__PURE__*/external_React_default().createElement("div", { 11096 className: "weatherText" 11097 }, /*#__PURE__*/external_React_default().createElement("div", { 11098 className: "weatherForecastRow" 11099 }, /*#__PURE__*/external_React_default().createElement("span", { 11100 className: "weatherTemperature" 11101 }, "22\xB0", Prefs.values["weather.temperatureUnits"])), /*#__PURE__*/external_React_default().createElement("div", { 11102 className: "weatherCityRow" 11103 }, /*#__PURE__*/external_React_default().createElement("span", { 11104 className: "weatherCity", 11105 "data-l10n-id": "newtab-weather-static-city" 11106 })))) : /*#__PURE__*/external_React_default().createElement("a", { 11107 "data-l10n-id": "newtab-weather-see-forecast", 11108 "data-l10n-args": "{\"provider\": \"AccuWeather\xAE\"}", 11109 href: WEATHER_SUGGESTION.forecast.url, 11110 className: "weatherInfoLink", 11111 onClick: this.onProviderClick 11112 }, /*#__PURE__*/external_React_default().createElement("div", { 11113 className: "weatherIconCol" 11114 }, /*#__PURE__*/external_React_default().createElement("span", { 11115 className: `weatherIcon iconId${WEATHER_SUGGESTION.current_conditions.icon_id}` 11116 })), /*#__PURE__*/external_React_default().createElement("div", { 11117 className: "weatherText" 11118 }, /*#__PURE__*/external_React_default().createElement("div", { 11119 className: "weatherForecastRow" 11120 }, /*#__PURE__*/external_React_default().createElement("span", { 11121 className: "weatherTemperature" 11122 }, WEATHER_SUGGESTION.current_conditions.temperature[Prefs.values["weather.temperatureUnits"]], "\xB0", Prefs.values["weather.temperatureUnits"])), /*#__PURE__*/external_React_default().createElement("div", { 11123 className: "weatherCityRow" 11124 }, /*#__PURE__*/external_React_default().createElement("span", { 11125 className: "weatherCity" 11126 }, Weather.locationData.city)), showDetailedView ? /*#__PURE__*/external_React_default().createElement("div", { 11127 className: "weatherDetailedSummaryRow" 11128 }, /*#__PURE__*/external_React_default().createElement("div", { 11129 className: "weatherHighLowTemps" 11130 }, /*#__PURE__*/external_React_default().createElement("span", null, WEATHER_SUGGESTION.forecast.high[Prefs.values["weather.temperatureUnits"]], "\xB0", Prefs.values["weather.temperatureUnits"]), /*#__PURE__*/external_React_default().createElement("span", null, "\u2022"), /*#__PURE__*/external_React_default().createElement("span", null, WEATHER_SUGGESTION.forecast.low[Prefs.values["weather.temperatureUnits"]], "\xB0", Prefs.values["weather.temperatureUnits"])), /*#__PURE__*/external_React_default().createElement("span", { 11131 className: "weatherTextSummary" 11132 }, WEATHER_SUGGESTION.current_conditions.summary)) : null)), contextMenu(showStaticData ? WEATHER_SOURCE_SHORTENED_CONTEXT_MENU_OPTIONS : WEATHER_SOURCE_CONTEXT_MENU_OPTIONS)), /*#__PURE__*/external_React_default().createElement("span", { 11133 className: "weatherSponsorText" 11134 }, /*#__PURE__*/external_React_default().createElement("span", { 11135 "data-l10n-id": "newtab-weather-sponsored", 11136 "data-l10n-args": "{\"provider\": \"AccuWeather\xAE\"}" 11137 })), shouldShowOptInDialog && /*#__PURE__*/external_React_default().createElement("div", { 11138 className: "weatherOptIn" 11139 }, /*#__PURE__*/external_React_default().createElement("dialog", { 11140 open: true 11141 }, /*#__PURE__*/external_React_default().createElement("span", { 11142 className: "weatherOptInImg" 11143 }), /*#__PURE__*/external_React_default().createElement("div", { 11144 className: "weatherOptInContent" 11145 }, /*#__PURE__*/external_React_default().createElement("h3", { 11146 "data-l10n-id": "newtab-weather-opt-in-see-weather" 11147 }), /*#__PURE__*/external_React_default().createElement("moz-button-group", { 11148 className: "button-group" 11149 }, /*#__PURE__*/external_React_default().createElement("moz-button", { 11150 size: "small", 11151 type: "default", 11152 "data-l10n-id": "newtab-weather-opt-in-yes", 11153 onClick: this.handleAcceptOptIn, 11154 id: "accept-opt-in", 11155 slot: reverseOptInButtons ? "" : "primary" 11156 }), /*#__PURE__*/external_React_default().createElement("moz-button", { 11157 size: "small", 11158 type: "default", 11159 "data-l10n-id": "newtab-weather-opt-in-not-now", 11160 onClick: this.handleRejectOptIn, 11161 id: "reject-opt-in", 11162 slot: reverseOptInButtons ? "primary" : "" 11163 })))))); 11164 } 11165 return /*#__PURE__*/external_React_default().createElement("div", { 11166 ref: this.setErrorRef, 11167 className: outerClassName 11168 }, /*#__PURE__*/external_React_default().createElement("div", { 11169 className: "weatherNotAvailable" 11170 }, /*#__PURE__*/external_React_default().createElement("span", { 11171 className: "icon icon-info-warning" 11172 }), " ", /*#__PURE__*/external_React_default().createElement("p", { 11173 "data-l10n-id": "newtab-weather-error-not-available" 11174 }), contextMenu(WEATHER_SOURCE_SHORTENED_CONTEXT_MENU_OPTIONS))); 11175 } 11176 } 11177 const Weather_Weather = (0,external_ReactRedux_namespaceObject.connect)(state => ({ 11178 App: state.App, 11179 Weather: state.Weather, 11180 Prefs: state.Prefs, 11181 IntersectionObserver: globalThis.IntersectionObserver, 11182 document: globalThis.document 11183 }))(_Weather); 11184 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/CardSections/CardSections.jsx 11185 /* This Source Code Form is subject to the terms of the Mozilla Public 11186 * License, v. 2.0. If a copy of the MPL was not distributed with this 11187 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 11188 11189 11190 11191 11192 11193 11194 11195 11196 11197 11198 11199 11200 11201 11202 11203 // Prefs 11204 const CardSections_PREF_SECTIONS_CARDS_ENABLED = "discoverystream.sections.cards.enabled"; 11205 const PREF_SECTIONS_PERSONALIZATION_ENABLED = "discoverystream.sections.personalization.enabled"; 11206 const CardSections_PREF_TOPICS_ENABLED = "discoverystream.topicLabels.enabled"; 11207 const CardSections_PREF_TOPICS_SELECTED = "discoverystream.topicSelection.selectedTopics"; 11208 const CardSections_PREF_TOPICS_AVAILABLE = "discoverystream.topicSelection.topics"; 11209 const PREF_INTEREST_PICKER_ENABLED = "discoverystream.sections.interestPicker.enabled"; 11210 const CardSections_PREF_VISIBLE_SECTIONS = "discoverystream.sections.interestPicker.visibleSections"; 11211 const CardSections_PREF_BILLBOARD_ENABLED = "newtabAdSize.billboard"; 11212 const CardSections_PREF_BILLBOARD_POSITION = "newtabAdSize.billboard.position"; 11213 const CardSections_PREF_LEADERBOARD_ENABLED = "newtabAdSize.leaderboard"; 11214 const CardSections_PREF_LEADERBOARD_POSITION = "newtabAdSize.leaderboard.position"; 11215 const PREF_REFINED_CARDS_ENABLED = "discoverystream.refinedCardsLayout.enabled"; 11216 const PREF_INFERRED_PERSONALIZATION_USER = "discoverystream.sections.personalization.inferred.user.enabled"; 11217 const CardSections_PREF_DAILY_BRIEF_SECTIONID = "discoverystream.dailyBrief.sectionId"; 11218 const CardSections_PREF_SPOCS_STARTUPCACHE_ENABLED = "discoverystream.spocs.startupCache.enabled"; 11219 function getLayoutData(responsiveLayouts, index, refinedCardsLayout) { 11220 let layoutData = { 11221 classNames: [], 11222 imageSizes: {} 11223 }; 11224 responsiveLayouts.forEach(layout => { 11225 layout.tiles.forEach((tile, tileIndex) => { 11226 if (tile.position === index) { 11227 layoutData.classNames.push(`col-${layout.columnCount}-${tile.size}`); 11228 layoutData.classNames.push(`col-${layout.columnCount}-position-${tileIndex}`); 11229 layoutData.imageSizes[layout.columnCount] = tile.size; 11230 11231 // The API tells us whether the tile should show the excerpt or not. 11232 // Apply extra styles accordingly. 11233 if (tile.hasExcerpt) { 11234 if (tile.size === "medium" && refinedCardsLayout) { 11235 layoutData.classNames.push(`col-${layout.columnCount}-hide-excerpt`); 11236 } else { 11237 layoutData.classNames.push(`col-${layout.columnCount}-show-excerpt`); 11238 } 11239 } else { 11240 layoutData.classNames.push(`col-${layout.columnCount}-hide-excerpt`); 11241 } 11242 } 11243 }); 11244 }); 11245 return layoutData; 11246 } 11247 11248 // function to determine amount of tiles shown per section per viewport 11249 function getMaxTiles(responsiveLayouts) { 11250 return responsiveLayouts.flatMap(responsiveLayout => responsiveLayout).reduce((acc, t) => { 11251 acc[t.columnCount] = t.tiles.length; 11252 11253 // Update maxTile if current tile count is greater 11254 if (!acc.maxTile || t.tiles.length > acc.maxTile) { 11255 acc.maxTile = t.tiles.length; 11256 } 11257 return acc; 11258 }, {}); 11259 } 11260 11261 /** 11262 * Transforms a comma-separated string in user preferences 11263 * into a cleaned-up array. 11264 * 11265 * @param {string} pref - The comma-separated pref to be converted. 11266 * @returns {string[]} An array of trimmed strings, excluding empty values. 11267 */ 11268 11269 const prefToArray = (pref = "") => { 11270 return pref.split(",").map(item => item.trim()).filter(item => item); 11271 }; 11272 function shouldShowOMCHighlight(messageData, componentId) { 11273 if (!messageData || Object.keys(messageData).length === 0) { 11274 return false; 11275 } 11276 return messageData?.content?.messageType === componentId; 11277 } 11278 function CardSection({ 11279 sectionPosition, 11280 section, 11281 dispatch, 11282 type, 11283 firstVisibleTimestamp, 11284 ctaButtonVariant, 11285 ctaButtonSponsors, 11286 anySectionsFollowed, 11287 showWeather, 11288 placeholder 11289 }) { 11290 const prefs = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Prefs.values); 11291 const { 11292 messageData 11293 } = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Messages); 11294 const { 11295 sectionPersonalization 11296 } = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.DiscoveryStream); 11297 const { 11298 isForStartupCache 11299 } = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.App); 11300 const [focusedIndex, setFocusedIndex] = (0,external_React_namespaceObject.useState)(0); 11301 const onCardFocus = index => { 11302 setFocusedIndex(index); 11303 }; 11304 const handleCardKeyDown = e => { 11305 if (e.key === "ArrowLeft" || e.key === "ArrowRight") { 11306 e.preventDefault(); 11307 const currentCardEl = e.target.closest("article.ds-card"); 11308 if (!currentCardEl) { 11309 return; 11310 } 11311 const activeColumn = getActiveColumnLayout(window.innerWidth); 11312 11313 // Arrow direction should match visual navigation direction in RTL 11314 const isRTL = document.dir === "rtl"; 11315 const navigateToPrevious = isRTL ? e.key === "ArrowRight" : e.key === "ArrowLeft"; 11316 11317 // Extract current position from classList 11318 let currentPosition = null; 11319 const positionPrefix = `${activeColumn}-position-`; 11320 for (let className of currentCardEl.classList) { 11321 if (className.startsWith(positionPrefix)) { 11322 currentPosition = parseInt(className.substring(positionPrefix.length), 10); 11323 break; 11324 } 11325 } 11326 if (currentPosition === null) { 11327 return; 11328 } 11329 const targetPosition = navigateToPrevious ? currentPosition - 1 : currentPosition + 1; 11330 11331 // Find card with target position 11332 const parentEl = currentCardEl.parentElement; 11333 if (parentEl) { 11334 const targetSelector = `article.ds-card.${activeColumn}-position-${targetPosition}`; 11335 const targetCardEl = parentEl.querySelector(targetSelector); 11336 if (targetCardEl) { 11337 const link = targetCardEl.querySelector("a.ds-card-link"); 11338 if (link) { 11339 link.focus(); 11340 } 11341 } 11342 } 11343 } 11344 }; 11345 const showTopics = prefs[CardSections_PREF_TOPICS_ENABLED]; 11346 const mayHaveSectionsCards = prefs[CardSections_PREF_SECTIONS_CARDS_ENABLED]; 11347 const selectedTopics = prefs[CardSections_PREF_TOPICS_SELECTED]; 11348 const availableTopics = prefs[CardSections_PREF_TOPICS_AVAILABLE]; 11349 const refinedCardsLayout = prefs[PREF_REFINED_CARDS_ENABLED]; 11350 const spocsStartupCacheEnabled = prefs[CardSections_PREF_SPOCS_STARTUPCACHE_ENABLED]; 11351 const mayHaveSectionsPersonalization = prefs[PREF_SECTIONS_PERSONALIZATION_ENABLED]; 11352 const { 11353 sectionKey, 11354 title, 11355 subtitle 11356 } = section; 11357 const { 11358 responsiveLayouts, 11359 name: layoutName 11360 } = section.layout; 11361 const following = sectionPersonalization[sectionKey]?.isFollowed; 11362 const handleIntersection = (0,external_React_namespaceObject.useCallback)(() => { 11363 dispatch(actionCreators.AlsoToMain({ 11364 type: actionTypes.CARD_SECTION_IMPRESSION, 11365 data: { 11366 section: sectionKey, 11367 section_position: sectionPosition, 11368 is_section_followed: following, 11369 layout_name: layoutName 11370 } 11371 })); 11372 }, [dispatch, sectionKey, sectionPosition, following, layoutName]); 11373 11374 // Ref to hold the section element 11375 const sectionRefs = useIntersectionObserver(handleIntersection); 11376 const onFollowClick = (0,external_React_namespaceObject.useCallback)(() => { 11377 const updatedSectionData = { 11378 ...sectionPersonalization, 11379 [sectionKey]: { 11380 isFollowed: true, 11381 isBlocked: false, 11382 followedAt: new Date().toISOString() 11383 } 11384 }; 11385 dispatch(actionCreators.AlsoToMain({ 11386 type: actionTypes.SECTION_PERSONALIZATION_SET, 11387 data: updatedSectionData 11388 })); 11389 // Telemetry Event Dispatch 11390 dispatch(actionCreators.OnlyToMain({ 11391 type: "FOLLOW_SECTION", 11392 data: { 11393 section: sectionKey, 11394 section_position: sectionPosition, 11395 event_source: "MOZ_BUTTON" 11396 } 11397 })); 11398 }, [dispatch, sectionPersonalization, sectionKey, sectionPosition]); 11399 const onUnfollowClick = (0,external_React_namespaceObject.useCallback)(() => { 11400 const updatedSectionData = { 11401 ...sectionPersonalization 11402 }; 11403 delete updatedSectionData[sectionKey]; 11404 dispatch(actionCreators.AlsoToMain({ 11405 type: actionTypes.SECTION_PERSONALIZATION_SET, 11406 data: updatedSectionData 11407 })); 11408 11409 // Telemetry Event Dispatch 11410 dispatch(actionCreators.OnlyToMain({ 11411 type: "UNFOLLOW_SECTION", 11412 data: { 11413 section: sectionKey, 11414 section_position: sectionPosition, 11415 event_source: "MOZ_BUTTON" 11416 } 11417 })); 11418 }, [dispatch, sectionPersonalization, sectionKey, sectionPosition]); 11419 let { 11420 maxTile 11421 } = getMaxTiles(responsiveLayouts); 11422 if (placeholder) { 11423 // We need a number that divides evenly by 2, 3, and 4. 11424 // So it can be displayed without orphans in grids with 2, 3, and 4 columns. 11425 maxTile = 12; 11426 } 11427 const displaySections = section.data.slice(0, maxTile); 11428 const isSectionEmpty = !displaySections?.length; 11429 const shouldShowLabels = sectionKey === "top_stories_section" && showTopics; 11430 if (isSectionEmpty) { 11431 return null; 11432 } 11433 const sectionContextWrapper = /*#__PURE__*/external_React_default().createElement("div", { 11434 className: "section-context-wrapper" 11435 }, /*#__PURE__*/external_React_default().createElement("div", { 11436 className: following ? "section-follow following" : "section-follow" 11437 }, !anySectionsFollowed && sectionPosition === 0 && shouldShowOMCHighlight(messageData, "FollowSectionButtonHighlight") && /*#__PURE__*/external_React_default().createElement(MessageWrapper, { 11438 dispatch: dispatch 11439 }, /*#__PURE__*/external_React_default().createElement(FollowSectionButtonHighlight, { 11440 verticalPosition: "inset-block-center", 11441 position: "arrow-inline-start", 11442 dispatch: dispatch, 11443 feature: "FEATURE_FOLLOW_SECTION_BUTTON", 11444 messageData: messageData 11445 })), !anySectionsFollowed && sectionPosition === 0 && shouldShowOMCHighlight(messageData, "FollowSectionButtonAltHighlight") && /*#__PURE__*/external_React_default().createElement(MessageWrapper, { 11446 dispatch: dispatch 11447 }, /*#__PURE__*/external_React_default().createElement(FollowSectionButtonHighlight, { 11448 verticalPosition: "inset-block-center", 11449 position: "arrow-inline-start", 11450 dispatch: dispatch, 11451 feature: "FEATURE_ALT_FOLLOW_SECTION_BUTTON" 11452 })), /*#__PURE__*/external_React_default().createElement("moz-button", { 11453 onClick: following ? onUnfollowClick : onFollowClick, 11454 type: "default", 11455 index: sectionPosition, 11456 section: sectionKey 11457 }, /*#__PURE__*/external_React_default().createElement("span", { 11458 className: "section-button-follow-text", 11459 "data-l10n-id": "newtab-section-follow-button" 11460 }), /*#__PURE__*/external_React_default().createElement("span", { 11461 className: "section-button-following-text", 11462 "data-l10n-id": "newtab-section-following-button" 11463 }), /*#__PURE__*/external_React_default().createElement("span", { 11464 className: "section-button-unfollow-text", 11465 "data-l10n-id": "newtab-section-unfollow-button" 11466 }))), /*#__PURE__*/external_React_default().createElement(SectionContextMenu, { 11467 dispatch: dispatch, 11468 index: sectionPosition, 11469 following: following, 11470 sectionPersonalization: sectionPersonalization, 11471 sectionKey: sectionKey, 11472 title: title, 11473 type: type, 11474 sectionPosition: sectionPosition 11475 })); 11476 return /*#__PURE__*/external_React_default().createElement("section", { 11477 className: "ds-section", 11478 ref: el => { 11479 sectionRefs.current[0] = el; 11480 } 11481 }, /*#__PURE__*/external_React_default().createElement("div", { 11482 className: "section-heading" 11483 }, /*#__PURE__*/external_React_default().createElement("div", { 11484 className: "section-heading-inline-start" 11485 }, /*#__PURE__*/external_React_default().createElement("div", { 11486 className: "section-title-wrapper" 11487 }, /*#__PURE__*/external_React_default().createElement("h2", { 11488 className: "section-title" 11489 }, title), subtitle && /*#__PURE__*/external_React_default().createElement("p", { 11490 className: "section-subtitle" 11491 }, subtitle)), showWeather && /*#__PURE__*/external_React_default().createElement(Weather_Weather, { 11492 isInSection: true 11493 })), mayHaveSectionsPersonalization ? sectionContextWrapper : null), /*#__PURE__*/external_React_default().createElement("div", { 11494 className: `ds-section-grid ds-card-grid`, 11495 onKeyDown: handleCardKeyDown 11496 }, section.data.slice(0, maxTile).map((rec, index) => { 11497 const layoutData = getLayoutData(responsiveLayouts, index, refinedCardsLayout); 11498 const { 11499 classNames, 11500 imageSizes 11501 } = layoutData; 11502 // Render a placeholder card when: 11503 // 1. No recommendation is available. 11504 // 2. The item is flagged as a placeholder. 11505 // 3. Spocs are loading for with spocs startup cache disabled. 11506 if (!rec || rec.placeholder || placeholder || rec.flight_id && !spocsStartupCacheEnabled && isForStartupCache.DiscoveryStream) { 11507 return /*#__PURE__*/external_React_default().createElement(PlaceholderDSCard, { 11508 key: `dscard-${index}` 11509 }); 11510 } 11511 const card = /*#__PURE__*/external_React_default().createElement(DSCard, { 11512 key: `dscard-${rec.id}`, 11513 pos: rec.pos, 11514 flightId: rec.flight_id, 11515 image_src: rec.image_src, 11516 raw_image_src: rec.raw_image_src, 11517 icon_src: rec.icon_src, 11518 word_count: rec.word_count, 11519 time_to_read: rec.time_to_read, 11520 title: rec.title, 11521 topic: rec.topic, 11522 features: rec.features, 11523 excerpt: rec.excerpt, 11524 url: rec.url, 11525 id: rec.id, 11526 shim: rec.shim, 11527 fetchTimestamp: rec.fetchTimestamp, 11528 type: type, 11529 context: rec.context, 11530 sponsor: rec.sponsor, 11531 sponsored_by_override: rec.sponsored_by_override, 11532 dispatch: dispatch, 11533 source: rec.domain, 11534 publisher: rec.publisher, 11535 pocket_id: rec.pocket_id, 11536 context_type: rec.context_type, 11537 bookmarkGuid: rec.bookmarkGuid, 11538 recommendation_id: rec.recommendation_id, 11539 firstVisibleTimestamp: firstVisibleTimestamp, 11540 corpus_item_id: rec.corpus_item_id, 11541 scheduled_corpus_item_id: rec.scheduled_corpus_item_id, 11542 recommended_at: rec.recommended_at, 11543 received_rank: rec.received_rank, 11544 format: rec.format, 11545 alt_text: rec.alt_text, 11546 mayHaveSectionsCards: mayHaveSectionsCards, 11547 showTopics: shouldShowLabels, 11548 selectedTopics: selectedTopics, 11549 availableTopics: availableTopics, 11550 ctaButtonSponsors: ctaButtonSponsors, 11551 ctaButtonVariant: ctaButtonVariant, 11552 sectionsClassNames: classNames.join(" "), 11553 sectionsCardImageSizes: imageSizes, 11554 section: sectionKey, 11555 sectionPosition: sectionPosition, 11556 sectionFollowed: following, 11557 sectionLayoutName: layoutName, 11558 isTimeSensitive: rec.isTimeSensitive, 11559 tabIndex: index === focusedIndex ? 0 : -1, 11560 onFocus: () => onCardFocus(index), 11561 attribution: rec.attribution 11562 }); 11563 return [card]; 11564 }))); 11565 } 11566 function CardSections({ 11567 data, 11568 feed, 11569 dispatch, 11570 type, 11571 firstVisibleTimestamp, 11572 ctaButtonVariant, 11573 ctaButtonSponsors, 11574 placeholder 11575 }) { 11576 const prefs = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Prefs.values); 11577 const { 11578 spocs, 11579 sectionPersonalization 11580 } = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.DiscoveryStream); 11581 const { 11582 messageData 11583 } = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Messages); 11584 const weatherPlacement = (0,external_ReactRedux_namespaceObject.useSelector)(selectWeatherPlacement); 11585 const dailyBriefSectionId = prefs.trainhopConfig?.dailyBriefing?.sectionId || prefs[CardSections_PREF_DAILY_BRIEF_SECTIONID]; 11586 const weatherEnabled = prefs.showWeather; 11587 const personalizationEnabled = prefs[PREF_SECTIONS_PERSONALIZATION_ENABLED]; 11588 const interestPickerEnabled = prefs[PREF_INTEREST_PICKER_ENABLED]; 11589 11590 // Handle a render before feed has been fetched by displaying nothing 11591 if (!data) { 11592 return null; 11593 } 11594 const visibleSections = prefToArray(prefs[CardSections_PREF_VISIBLE_SECTIONS]); 11595 const { 11596 interestPicker 11597 } = data; 11598 11599 // Used to determine if we should show FollowSectionButtonHighlight 11600 const anySectionsFollowed = sectionPersonalization && Object.values(sectionPersonalization).some(section => section?.isFollowed); 11601 let sectionsData = data.sections; 11602 if (placeholder) { 11603 // To clean up the placeholder state for sections if the whole section is loading still. 11604 sectionsData = [{ 11605 ...sectionsData[0], 11606 title: "", 11607 subtitle: "" 11608 }, { 11609 ...sectionsData[1], 11610 title: "", 11611 subtitle: "" 11612 }]; 11613 } 11614 let filteredSections = sectionsData.filter(section => !sectionPersonalization[section.sectionKey]?.isBlocked); 11615 if (interestPickerEnabled && visibleSections.length) { 11616 filteredSections = visibleSections.reduce((acc, visibleSection) => { 11617 const found = filteredSections.find(({ 11618 sectionKey 11619 }) => sectionKey === visibleSection); 11620 if (found) { 11621 acc.push(found); 11622 } 11623 return acc; 11624 }, []); 11625 } 11626 let sectionsToRender = filteredSections.map((section, sectionPosition) => /*#__PURE__*/external_React_default().createElement(CardSection, { 11627 key: `section-${section.sectionKey}`, 11628 sectionPosition: sectionPosition, 11629 section: section, 11630 dispatch: dispatch, 11631 type: type, 11632 firstVisibleTimestamp: firstVisibleTimestamp, 11633 ctaButtonVariant: ctaButtonVariant, 11634 ctaButtonSponsors: ctaButtonSponsors, 11635 anySectionsFollowed: anySectionsFollowed, 11636 placeholder: placeholder, 11637 showWeather: weatherEnabled && weatherPlacement === "section" && sectionPosition === 0 && section.sectionKey === dailyBriefSectionId 11638 })); 11639 11640 // Add a billboard/leaderboard IAB ad to the sectionsToRender array (if enabled/possible). 11641 const billboardEnabled = prefs[CardSections_PREF_BILLBOARD_ENABLED]; 11642 const leaderboardEnabled = prefs[CardSections_PREF_LEADERBOARD_ENABLED]; 11643 if ((billboardEnabled || leaderboardEnabled) && spocs?.data?.newtab_spocs?.items) { 11644 const spocToRender = spocs.data.newtab_spocs.items.find(({ 11645 format 11646 }) => format === "leaderboard" && leaderboardEnabled) || spocs.data.newtab_spocs.items.find(({ 11647 format 11648 }) => format === "billboard" && billboardEnabled); 11649 if (spocToRender && !spocs.blocked.includes(spocToRender.url)) { 11650 const row = spocToRender.format === "leaderboard" ? prefs[CardSections_PREF_LEADERBOARD_POSITION] : prefs[CardSections_PREF_BILLBOARD_POSITION]; 11651 sectionsToRender.splice( 11652 // Math.min is used here to ensure the given row stays within the bounds of the sectionsToRender array. 11653 Math.min(sectionsToRender.length - 1, row), 0, /*#__PURE__*/external_React_default().createElement(AdBanner, { 11654 spoc: spocToRender, 11655 key: `dscard-${spocToRender.id}`, 11656 dispatch: dispatch, 11657 type: type, 11658 firstVisibleTimestamp: firstVisibleTimestamp, 11659 row: row, 11660 prefs: prefs 11661 })); 11662 } 11663 } 11664 11665 // Add the interest picker to the sectionsToRender array (if enabled/possible). 11666 if (interestPickerEnabled && personalizationEnabled && interestPicker?.sections) { 11667 const index = interestPicker.receivedFeedRank - 1; 11668 sectionsToRender.splice( 11669 // Math.min is used here to ensure the given row stays within the bounds of the sectionsToRender array. 11670 Math.min(sectionsToRender.length - 1, index), 0, /*#__PURE__*/external_React_default().createElement(InterestPicker, { 11671 title: interestPicker.title, 11672 subtitle: interestPicker.subtitle, 11673 interests: interestPicker.sections || [], 11674 receivedFeedRank: interestPicker.receivedFeedRank 11675 })); 11676 } 11677 function displayP13nCard() { 11678 if (messageData && Object.keys(messageData).length >= 1) { 11679 if (shouldShowOMCHighlight(messageData, "PersonalizedCard") && prefs[PREF_INFERRED_PERSONALIZATION_USER]) { 11680 const row = messageData.content.position; 11681 sectionsToRender.splice(row, 0, /*#__PURE__*/external_React_default().createElement(MessageWrapper, { 11682 dispatch: dispatch, 11683 onDismiss: () => {} 11684 }, /*#__PURE__*/external_React_default().createElement(PersonalizedCard, { 11685 position: row, 11686 dispatch: dispatch, 11687 messageData: messageData 11688 }))); 11689 } 11690 } 11691 } 11692 displayP13nCard(); 11693 const isEmpty = sectionsToRender.length === 0; 11694 return isEmpty ? /*#__PURE__*/external_React_default().createElement("div", { 11695 className: "ds-card-grid empty" 11696 }, /*#__PURE__*/external_React_default().createElement(DSEmptyState, { 11697 status: data.status, 11698 dispatch: dispatch, 11699 feed: feed 11700 })) : /*#__PURE__*/external_React_default().createElement("div", { 11701 className: "ds-section-wrapper" 11702 }, sectionsToRender); 11703 } 11704 11705 ;// CONCATENATED MODULE: ./content-src/components/Widgets/Lists/Lists.jsx 11706 function Lists_extends() { return Lists_extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, Lists_extends.apply(null, arguments); } 11707 /* This Source Code Form is subject to the terms of the Mozilla Public 11708 * License, v. 2.0. If a copy of the MPL was not distributed with this 11709 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 11710 11711 11712 11713 11714 11715 const TASK_TYPE = { 11716 IN_PROGRESS: "tasks", 11717 COMPLETED: "completed" 11718 }; 11719 const USER_ACTION_TYPES = { 11720 LIST_COPY: "list_copy", 11721 LIST_CREATE: "list_create", 11722 LIST_EDIT: "list_edit", 11723 LIST_DELETE: "list_delete", 11724 TASK_CREATE: "task_create", 11725 TASK_EDIT: "task_edit", 11726 TASK_DELETE: "task_delete", 11727 TASK_COMPLETE: "task_complete" 11728 }; 11729 const PREF_WIDGETS_LISTS_MAX_LISTS = "widgets.lists.maxLists"; 11730 const PREF_WIDGETS_LISTS_MAX_LISTITEMS = "widgets.lists.maxListItems"; 11731 const PREF_WIDGETS_LISTS_BADGE_ENABLED = "widgets.lists.badge.enabled"; 11732 const PREF_WIDGETS_LISTS_BADGE_LABEL = "widgets.lists.badge.label"; 11733 function Lists({ 11734 dispatch, 11735 handleUserInteraction, 11736 isMaximized 11737 }) { 11738 const prefs = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Prefs.values); 11739 const { 11740 selected, 11741 lists 11742 } = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.ListsWidget); 11743 const [newTask, setNewTask] = (0,external_React_namespaceObject.useState)(""); 11744 const [isEditing, setIsEditing] = (0,external_React_namespaceObject.useState)(false); 11745 const [pendingNewList, setPendingNewList] = (0,external_React_namespaceObject.useState)(null); 11746 const selectedList = (0,external_React_namespaceObject.useMemo)(() => lists[selected], [lists, selected]); 11747 const prevCompletedCount = (0,external_React_namespaceObject.useRef)(selectedList?.completed?.length || 0); 11748 const inputRef = (0,external_React_namespaceObject.useRef)(null); 11749 const selectRef = (0,external_React_namespaceObject.useRef)(null); 11750 const reorderListRef = (0,external_React_namespaceObject.useRef)(null); 11751 const [canvasRef, fireConfetti] = useConfetti(); 11752 const handleListInteraction = (0,external_React_namespaceObject.useCallback)(() => handleUserInteraction("lists"), [handleUserInteraction]); 11753 11754 // store selectedList with useMemo so it isnt re-calculated on every re-render 11755 const isValidUrl = (0,external_React_namespaceObject.useCallback)(str => URL.canParse(str), []); 11756 const handleIntersection = (0,external_React_namespaceObject.useCallback)(() => { 11757 dispatch(actionCreators.AlsoToMain({ 11758 type: actionTypes.WIDGETS_LISTS_USER_IMPRESSION 11759 })); 11760 }, [dispatch]); 11761 const listsRef = useIntersectionObserver(handleIntersection); 11762 const reorderLists = (0,external_React_namespaceObject.useCallback)((draggedElement, targetElement, before = false) => { 11763 const draggedIndex = selectedList.tasks.findIndex(({ 11764 id 11765 }) => id === draggedElement.id); 11766 const targetIndex = selectedList.tasks.findIndex(({ 11767 id 11768 }) => id === targetElement.id); 11769 11770 // return early is index is not found 11771 if (draggedIndex === -1 || targetIndex === -1 || draggedIndex === targetIndex) { 11772 return; 11773 } 11774 const reordered = [...selectedList.tasks]; 11775 const [removed] = reordered.splice(draggedIndex, 1); 11776 const insertIndex = before ? targetIndex : targetIndex + 1; 11777 reordered.splice(insertIndex > draggedIndex ? insertIndex - 1 : insertIndex, 0, removed); 11778 const updatedLists = { 11779 ...lists, 11780 [selected]: { 11781 ...selectedList, 11782 tasks: reordered 11783 } 11784 }; 11785 dispatch(actionCreators.AlsoToMain({ 11786 type: actionTypes.WIDGETS_LISTS_UPDATE, 11787 data: { 11788 lists: updatedLists 11789 } 11790 })); 11791 handleListInteraction(); 11792 }, [lists, selected, selectedList, dispatch, handleListInteraction]); 11793 const moveTask = (0,external_React_namespaceObject.useCallback)((task, direction) => { 11794 const index = selectedList.tasks.findIndex(({ 11795 id 11796 }) => id === task.id); 11797 11798 // guardrail a falsey index 11799 if (index === -1) { 11800 return; 11801 } 11802 const targetIndex = direction === "up" ? index - 1 : index + 1; 11803 const before = direction === "up"; 11804 const targetTask = selectedList.tasks[targetIndex]; 11805 if (targetTask) { 11806 reorderLists(task, targetTask, before); 11807 } 11808 }, [selectedList, reorderLists]); 11809 (0,external_React_namespaceObject.useEffect)(() => { 11810 const selectNode = selectRef.current; 11811 const reorderNode = reorderListRef.current; 11812 if (!selectNode || !reorderNode) { 11813 return undefined; 11814 } 11815 function handleSelectChange(e) { 11816 dispatch(actionCreators.AlsoToMain({ 11817 type: actionTypes.WIDGETS_LISTS_CHANGE_SELECTED, 11818 data: e.target.value 11819 })); 11820 handleListInteraction(); 11821 } 11822 function handleReorder(e) { 11823 const { 11824 draggedElement, 11825 targetElement, 11826 position 11827 } = e.detail; 11828 reorderLists(draggedElement, targetElement, position === -1); 11829 } 11830 reorderNode.addEventListener("reorder", handleReorder); 11831 selectNode.addEventListener("change", handleSelectChange); 11832 return () => { 11833 selectNode.removeEventListener("change", handleSelectChange); 11834 reorderNode.removeEventListener("reorder", handleReorder); 11835 }; 11836 }, [dispatch, isEditing, reorderLists, handleListInteraction]); 11837 11838 // effect that enables editing new list name only after store has been hydrated 11839 (0,external_React_namespaceObject.useEffect)(() => { 11840 if (selected === pendingNewList) { 11841 setIsEditing(true); 11842 setPendingNewList(null); 11843 } 11844 }, [selected, pendingNewList]); 11845 function saveTask() { 11846 const trimmedTask = newTask.trimEnd(); 11847 // only add new task if it has a length, to avoid creating empty tasks 11848 if (trimmedTask) { 11849 const formattedTask = { 11850 value: trimmedTask, 11851 completed: false, 11852 created: Date.now(), 11853 id: crypto.randomUUID(), 11854 isUrl: isValidUrl(trimmedTask) 11855 }; 11856 const updatedLists = { 11857 ...lists, 11858 [selected]: { 11859 ...selectedList, 11860 tasks: [formattedTask, ...lists[selected].tasks] 11861 } 11862 }; 11863 (0,external_ReactRedux_namespaceObject.batch)(() => { 11864 dispatch(actionCreators.AlsoToMain({ 11865 type: actionTypes.WIDGETS_LISTS_UPDATE, 11866 data: { 11867 lists: updatedLists 11868 } 11869 })); 11870 dispatch(actionCreators.OnlyToMain({ 11871 type: actionTypes.WIDGETS_LISTS_USER_EVENT, 11872 data: { 11873 userAction: USER_ACTION_TYPES.TASK_CREATE 11874 } 11875 })); 11876 }); 11877 setNewTask(""); 11878 handleListInteraction(); 11879 } 11880 } 11881 function updateTask(updatedTask, type) { 11882 const isCompletedType = type === TASK_TYPE.COMPLETED; 11883 const isNowCompleted = updatedTask.completed; 11884 let newTasks = selectedList.tasks; 11885 let newCompleted = selectedList.completed; 11886 let userAction; 11887 11888 // If the task is in the completed array and is now unchecked 11889 const shouldMoveToTasks = isCompletedType && !isNowCompleted; 11890 11891 // If we're moving the task from tasks → completed (user checked it) 11892 const shouldMoveToCompleted = !isCompletedType && isNowCompleted; 11893 11894 // Move task from completed -> task 11895 if (shouldMoveToTasks) { 11896 newCompleted = selectedList.completed.filter(task => task.id !== updatedTask.id); 11897 newTasks = [...selectedList.tasks, updatedTask]; 11898 // Move task to completed, but also create local version 11899 } else if (shouldMoveToCompleted) { 11900 newTasks = selectedList.tasks.filter(task => task.id !== updatedTask.id); 11901 newCompleted = [...selectedList.completed, updatedTask]; 11902 userAction = USER_ACTION_TYPES.TASK_COMPLETE; 11903 } else { 11904 const targetKey = isCompletedType ? "completed" : "tasks"; 11905 const updatedArray = selectedList[targetKey].map(task => task.id === updatedTask.id ? updatedTask : task); 11906 // In-place update: toggle checkbox (but stay in same array or edit name) 11907 if (targetKey === "tasks") { 11908 newTasks = updatedArray; 11909 } else { 11910 newCompleted = updatedArray; 11911 } 11912 userAction = USER_ACTION_TYPES.TASK_EDIT; 11913 } 11914 const updatedLists = { 11915 ...lists, 11916 [selected]: { 11917 ...selectedList, 11918 tasks: newTasks, 11919 completed: newCompleted 11920 } 11921 }; 11922 (0,external_ReactRedux_namespaceObject.batch)(() => { 11923 dispatch(actionCreators.AlsoToMain({ 11924 type: actionTypes.WIDGETS_LISTS_UPDATE, 11925 data: { 11926 lists: updatedLists 11927 } 11928 })); 11929 if (userAction) { 11930 dispatch(actionCreators.AlsoToMain({ 11931 type: actionTypes.WIDGETS_LISTS_USER_EVENT, 11932 data: { 11933 userAction 11934 } 11935 })); 11936 } 11937 }); 11938 handleListInteraction(); 11939 } 11940 function deleteTask(task, type) { 11941 const selectedTasks = lists[selected][type]; 11942 const updatedTasks = selectedTasks.filter(({ 11943 id 11944 }) => id !== task.id); 11945 const updatedLists = { 11946 ...lists, 11947 [selected]: { 11948 ...selectedList, 11949 [type]: updatedTasks 11950 } 11951 }; 11952 (0,external_ReactRedux_namespaceObject.batch)(() => { 11953 dispatch(actionCreators.AlsoToMain({ 11954 type: actionTypes.WIDGETS_LISTS_UPDATE, 11955 data: { 11956 lists: updatedLists 11957 } 11958 })); 11959 dispatch(actionCreators.OnlyToMain({ 11960 type: actionTypes.WIDGETS_LISTS_USER_EVENT, 11961 data: { 11962 userAction: USER_ACTION_TYPES.TASK_DELETE 11963 } 11964 })); 11965 }); 11966 handleListInteraction(); 11967 } 11968 function handleKeyDown(e) { 11969 if (e.key === "Enter" && document.activeElement === inputRef.current) { 11970 saveTask(); 11971 } else if (e.key === "Escape" && document.activeElement === inputRef.current) { 11972 // Clear out the input when esc is pressed 11973 setNewTask(""); 11974 } 11975 } 11976 function handleListNameSave(newLabel) { 11977 const trimmedLabel = newLabel.trimEnd(); 11978 if (trimmedLabel && trimmedLabel !== selectedList?.label) { 11979 const updatedLists = { 11980 ...lists, 11981 [selected]: { 11982 ...selectedList, 11983 label: trimmedLabel 11984 } 11985 }; 11986 (0,external_ReactRedux_namespaceObject.batch)(() => { 11987 dispatch(actionCreators.AlsoToMain({ 11988 type: actionTypes.WIDGETS_LISTS_UPDATE, 11989 data: { 11990 lists: updatedLists 11991 } 11992 })); 11993 dispatch(actionCreators.OnlyToMain({ 11994 type: actionTypes.WIDGETS_LISTS_USER_EVENT, 11995 data: { 11996 userAction: USER_ACTION_TYPES.LIST_EDIT 11997 } 11998 })); 11999 }); 12000 setIsEditing(false); 12001 handleListInteraction(); 12002 } 12003 } 12004 function handleCreateNewList() { 12005 const id = crypto.randomUUID(); 12006 const newLists = { 12007 ...lists, 12008 [id]: { 12009 label: "", 12010 tasks: [], 12011 completed: [] 12012 } 12013 }; 12014 (0,external_ReactRedux_namespaceObject.batch)(() => { 12015 dispatch(actionCreators.AlsoToMain({ 12016 type: actionTypes.WIDGETS_LISTS_UPDATE, 12017 data: { 12018 lists: newLists 12019 } 12020 })); 12021 dispatch(actionCreators.AlsoToMain({ 12022 type: actionTypes.WIDGETS_LISTS_CHANGE_SELECTED, 12023 data: id 12024 })); 12025 dispatch(actionCreators.OnlyToMain({ 12026 type: actionTypes.WIDGETS_LISTS_USER_EVENT, 12027 data: { 12028 userAction: USER_ACTION_TYPES.LIST_CREATE 12029 } 12030 })); 12031 }); 12032 setPendingNewList(id); 12033 handleListInteraction(); 12034 } 12035 function handleCancelNewList() { 12036 // If current list is new and has no label/tasks, remove it 12037 if (!selectedList?.label && selectedList?.tasks?.length === 0) { 12038 const updatedLists = { 12039 ...lists 12040 }; 12041 delete updatedLists[selected]; 12042 const listKeys = Object.keys(updatedLists); 12043 const key = listKeys[listKeys.length - 1]; 12044 (0,external_ReactRedux_namespaceObject.batch)(() => { 12045 dispatch(actionCreators.AlsoToMain({ 12046 type: actionTypes.WIDGETS_LISTS_UPDATE, 12047 data: { 12048 lists: updatedLists 12049 } 12050 })); 12051 dispatch(actionCreators.AlsoToMain({ 12052 type: actionTypes.WIDGETS_LISTS_CHANGE_SELECTED, 12053 data: key 12054 })); 12055 dispatch(actionCreators.OnlyToMain({ 12056 type: actionTypes.WIDGETS_LISTS_USER_EVENT, 12057 data: { 12058 userAction: USER_ACTION_TYPES.LIST_DELETE 12059 } 12060 })); 12061 }); 12062 } 12063 handleListInteraction(); 12064 } 12065 function handleDeleteList() { 12066 let updatedLists = { 12067 ...lists 12068 }; 12069 if (updatedLists[selected]) { 12070 delete updatedLists[selected]; 12071 12072 // if this list was the last one created, add a new list as default 12073 if (Object.keys(updatedLists)?.length === 0) { 12074 updatedLists = { 12075 [crypto.randomUUID()]: { 12076 label: "", 12077 tasks: [], 12078 completed: [] 12079 } 12080 }; 12081 } 12082 const listKeys = Object.keys(updatedLists); 12083 const key = listKeys[listKeys.length - 1]; 12084 (0,external_ReactRedux_namespaceObject.batch)(() => { 12085 dispatch(actionCreators.AlsoToMain({ 12086 type: actionTypes.WIDGETS_LISTS_UPDATE, 12087 data: { 12088 lists: updatedLists 12089 } 12090 })); 12091 dispatch(actionCreators.AlsoToMain({ 12092 type: actionTypes.WIDGETS_LISTS_CHANGE_SELECTED, 12093 data: key 12094 })); 12095 dispatch(actionCreators.OnlyToMain({ 12096 type: actionTypes.WIDGETS_LISTS_USER_EVENT, 12097 data: { 12098 userAction: USER_ACTION_TYPES.LIST_DELETE 12099 } 12100 })); 12101 }); 12102 } 12103 handleListInteraction(); 12104 } 12105 function handleHideLists() { 12106 dispatch(actionCreators.OnlyToMain({ 12107 type: actionTypes.SET_PREF, 12108 data: { 12109 name: "widgets.lists.enabled", 12110 value: false 12111 } 12112 })); 12113 handleListInteraction(); 12114 } 12115 function handleCopyListToClipboard() { 12116 const currentList = lists[selected]; 12117 if (!currentList) { 12118 return; 12119 } 12120 const { 12121 label, 12122 tasks = [], 12123 completed = [] 12124 } = currentList; 12125 const uncompleted = tasks.filter(task => !task.completed); 12126 const currentCompleted = tasks.filter(task => task.completed); 12127 12128 // In order in include all items, we need to iterate through both current and completed tasks list and mark format all completed tasks accordingly. 12129 const formatted = [`List: ${label}`, `---`, ...uncompleted.map(task => `- [ ] ${task.value}`), ...currentCompleted.map(task => `- [x] ${task.value}`), ...completed.map(task => `- [x] ${task.value}`)].join("\n"); 12130 try { 12131 navigator.clipboard.writeText(formatted); 12132 } catch (err) { 12133 console.error("Copy failed", err); 12134 } 12135 dispatch(actionCreators.OnlyToMain({ 12136 type: actionTypes.WIDGETS_LISTS_USER_EVENT, 12137 data: { 12138 userAction: USER_ACTION_TYPES.LIST_COPY 12139 } 12140 })); 12141 handleListInteraction(); 12142 } 12143 function handleLearnMore() { 12144 dispatch(actionCreators.OnlyToMain({ 12145 type: actionTypes.OPEN_LINK, 12146 data: { 12147 url: "https://support.mozilla.org/kb/firefox-new-tab-widgets" 12148 } 12149 })); 12150 handleListInteraction(); 12151 } 12152 12153 // Reset baseline only when switching lists 12154 (0,external_React_namespaceObject.useEffect)(() => { 12155 prevCompletedCount.current = selectedList?.completed?.length || 0; 12156 // intentionally leaving out selectedList from dependency array 12157 // eslint-disable-next-line react-hooks/exhaustive-deps 12158 }, [selected]); 12159 (0,external_React_namespaceObject.useEffect)(() => { 12160 if (selectedList) { 12161 const doneCount = selectedList.completed?.length || 0; 12162 const previous = Math.floor(prevCompletedCount.current / 5); 12163 const current = Math.floor(doneCount / 5); 12164 if (current > previous) { 12165 fireConfetti(); 12166 } 12167 prevCompletedCount.current = doneCount; 12168 } 12169 }, [selectedList, fireConfetti, selected]); 12170 if (!lists) { 12171 return null; 12172 } 12173 12174 // Enforce maximum count limits to lists 12175 const currentListsCount = Object.keys(lists).length; 12176 // Ensure a minimum of 1, but allow higher values from prefs 12177 const maxListsCount = Math.max(1, prefs[PREF_WIDGETS_LISTS_MAX_LISTS]); 12178 const isAtMaxListsLimit = currentListsCount >= maxListsCount; 12179 12180 // Enforce maximum count limits to list items 12181 // The maximum applies to the total number of items (both incomplete and completed items) 12182 const currentSelectedListItemsCount = selectedList?.tasks.length + selectedList?.completed.length; 12183 12184 // Ensure a minimum of 1, but allow higher values from prefs 12185 const maxListItemsCount = Math.max(1, prefs[PREF_WIDGETS_LISTS_MAX_LISTITEMS]); 12186 const isAtMaxListItemsLimit = currentSelectedListItemsCount >= maxListItemsCount; 12187 12188 // Figure out if the selected list is the first (default) or a new one. 12189 // Index 0 → use "Task list"; any later index → use "New list". 12190 // Fallback to 0 if the selected id isn’t found. 12191 const listKeys = Object.keys(lists); 12192 const selectedIndex = Math.max(0, listKeys.indexOf(selected)); 12193 const listNamePlaceholder = currentListsCount > 1 && selectedIndex !== 0 ? "newtab-widget-lists-name-placeholder-new" : "newtab-widget-lists-name-placeholder-default"; 12194 const nimbusBadgeEnabled = prefs.widgetsConfig?.listsBadgeEnabled; 12195 const nimbusBadgeLabel = prefs.widgetsConfig?.listsBadgeLabel; 12196 const nimbusBadgeTrainhopEnabled = prefs.trainhopConfig?.widgets?.listsBadgeEnabled; 12197 const nimbusBadgeTrainhopLabel = prefs.trainhopConfig?.widgets?.listsBadgeLabel; 12198 const badgeEnabled = (nimbusBadgeEnabled || nimbusBadgeTrainhopEnabled) ?? prefs[PREF_WIDGETS_LISTS_BADGE_ENABLED] ?? false; 12199 const badgeLabel = (nimbusBadgeLabel || nimbusBadgeTrainhopLabel) ?? prefs[PREF_WIDGETS_LISTS_BADGE_LABEL] ?? ""; 12200 return /*#__PURE__*/external_React_default().createElement("article", { 12201 className: `lists ${isMaximized ? "is-maximized" : ""}`, 12202 ref: el => { 12203 listsRef.current = [el]; 12204 } 12205 }, /*#__PURE__*/external_React_default().createElement("div", { 12206 className: "select-wrapper" 12207 }, /*#__PURE__*/external_React_default().createElement(EditableText, { 12208 value: lists[selected]?.label || "", 12209 onSave: handleListNameSave, 12210 isEditing: isEditing, 12211 setIsEditing: setIsEditing, 12212 onCancel: handleCancelNewList, 12213 type: "list", 12214 maxLength: 30, 12215 dataL10nId: listNamePlaceholder 12216 }, /*#__PURE__*/external_React_default().createElement("moz-select", { 12217 ref: selectRef, 12218 value: selected 12219 }, Object.entries(lists).map(([key, list]) => /*#__PURE__*/external_React_default().createElement("moz-option", Lists_extends({ 12220 key: key, 12221 value: key 12222 // On the first/initial list, use default name 12223 }, list.label ? { 12224 label: list.label 12225 } : { 12226 "data-l10n-id": "newtab-widget-lists-name-label-default" 12227 }))))), !isEditing && badgeEnabled && badgeLabel && /*#__PURE__*/external_React_default().createElement("moz-badge", { 12228 "data-l10n-id": (() => { 12229 if (badgeLabel === "New") { 12230 return "newtab-widget-lists-label-new"; 12231 } 12232 if (badgeLabel === "Beta") { 12233 return "newtab-widget-lists-label-beta"; 12234 } 12235 return ""; 12236 })() 12237 }), /*#__PURE__*/external_React_default().createElement("moz-button", { 12238 className: "lists-panel-button", 12239 iconSrc: "chrome://global/skin/icons/more.svg", 12240 menuId: "lists-panel", 12241 type: "ghost" 12242 }), /*#__PURE__*/external_React_default().createElement("panel-list", { 12243 id: "lists-panel" 12244 }, /*#__PURE__*/external_React_default().createElement("panel-item", { 12245 "data-l10n-id": "newtab-widget-lists-menu-edit", 12246 onClick: () => setIsEditing(true) 12247 }), /*#__PURE__*/external_React_default().createElement("panel-item", Lists_extends({}, isAtMaxListsLimit ? { 12248 disabled: true 12249 } : {}, { 12250 "data-l10n-id": "newtab-widget-lists-menu-create", 12251 onClick: () => handleCreateNewList(), 12252 className: "create-list" 12253 })), /*#__PURE__*/external_React_default().createElement("panel-item", { 12254 "data-l10n-id": "newtab-widget-lists-menu-delete", 12255 onClick: () => handleDeleteList() 12256 }), /*#__PURE__*/external_React_default().createElement("hr", null), /*#__PURE__*/external_React_default().createElement("panel-item", { 12257 "data-l10n-id": "newtab-widget-lists-menu-copy", 12258 onClick: () => handleCopyListToClipboard() 12259 }), /*#__PURE__*/external_React_default().createElement("panel-item", { 12260 "data-l10n-id": "newtab-widget-lists-menu-hide", 12261 onClick: () => handleHideLists() 12262 }), /*#__PURE__*/external_React_default().createElement("panel-item", { 12263 className: "learn-more", 12264 "data-l10n-id": "newtab-widget-lists-menu-learn-more", 12265 onClick: handleLearnMore 12266 }))), /*#__PURE__*/external_React_default().createElement("div", { 12267 className: "add-task-container" 12268 }, /*#__PURE__*/external_React_default().createElement("span", { 12269 className: `icon icon-add ${isAtMaxListItemsLimit ? "icon-disabled" : ""}` 12270 }), /*#__PURE__*/external_React_default().createElement("input", { 12271 ref: inputRef, 12272 onBlur: () => saveTask(), 12273 onChange: e => setNewTask(e.target.value), 12274 value: newTask, 12275 "data-l10n-id": "newtab-widget-lists-input-add-an-item", 12276 className: "add-task-input", 12277 onKeyDown: handleKeyDown, 12278 type: "text", 12279 maxLength: 100, 12280 disabled: isAtMaxListItemsLimit 12281 })), /*#__PURE__*/external_React_default().createElement("div", { 12282 className: "task-list-wrapper" 12283 }, /*#__PURE__*/external_React_default().createElement("moz-reorderable-list", { 12284 ref: reorderListRef, 12285 itemSelector: "fieldset .task-type-tasks", 12286 dragSelector: ".checkbox-wrapper:has(.task-label)" 12287 }, /*#__PURE__*/external_React_default().createElement("fieldset", null, selectedList?.tasks.length >= 1 && selectedList.tasks.map((task, index) => /*#__PURE__*/external_React_default().createElement(ListItem, { 12288 type: TASK_TYPE.IN_PROGRESS, 12289 task: task, 12290 key: task.id, 12291 updateTask: updateTask, 12292 deleteTask: deleteTask, 12293 moveTask: moveTask, 12294 isValidUrl: isValidUrl, 12295 isFirst: index === 0, 12296 isLast: index === selectedList.tasks.length - 1 12297 })), selectedList?.completed.length >= 1 && /*#__PURE__*/external_React_default().createElement("details", { 12298 className: "completed-task-wrapper", 12299 open: selectedList?.tasks.length < 1 12300 }, /*#__PURE__*/external_React_default().createElement("summary", null, /*#__PURE__*/external_React_default().createElement("span", { 12301 "data-l10n-id": "newtab-widget-lists-completed-list", 12302 "data-l10n-args": JSON.stringify({ 12303 number: lists[selected]?.completed.length 12304 }), 12305 className: "completed-title" 12306 })), selectedList?.completed.map(completedTask => /*#__PURE__*/external_React_default().createElement(ListItem, { 12307 key: completedTask.id, 12308 type: TASK_TYPE.COMPLETED, 12309 task: completedTask, 12310 deleteTask: deleteTask, 12311 updateTask: updateTask 12312 }))))), selectedList?.tasks.length < 1 && selectedList?.completed.length < 1 && /*#__PURE__*/external_React_default().createElement("div", { 12313 className: "empty-list" 12314 }, /*#__PURE__*/external_React_default().createElement("picture", null, /*#__PURE__*/external_React_default().createElement("source", { 12315 srcSet: "chrome://newtab/content/data/content/assets/lists-empty-state-dark.svg", 12316 media: "(prefers-color-scheme: dark)" 12317 }), /*#__PURE__*/external_React_default().createElement("source", { 12318 srcSet: "chrome://newtab/content/data/content/assets/lists-empty-state-light.svg", 12319 media: "(prefers-color-scheme: light)" 12320 }), /*#__PURE__*/external_React_default().createElement("img", { 12321 width: "100", 12322 height: "100", 12323 alt: "" 12324 })), /*#__PURE__*/external_React_default().createElement("p", { 12325 className: "empty-list-text", 12326 "data-l10n-id": "newtab-widget-lists-empty-cta" 12327 }))), /*#__PURE__*/external_React_default().createElement("canvas", { 12328 className: "confetti-canvas", 12329 ref: canvasRef 12330 })); 12331 } 12332 function ListItem({ 12333 task, 12334 updateTask, 12335 deleteTask, 12336 moveTask, 12337 isValidUrl, 12338 type, 12339 isFirst = false, 12340 isLast = false 12341 }) { 12342 const [isEditing, setIsEditing] = (0,external_React_namespaceObject.useState)(false); 12343 const [exiting, setExiting] = (0,external_React_namespaceObject.useState)(false); 12344 const isCompleted = type === TASK_TYPE.COMPLETED; 12345 const prefersReducedMotion = typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches; 12346 function handleCheckboxChange(e) { 12347 const { 12348 checked 12349 } = e.target; 12350 const updatedTask = { 12351 ...task, 12352 completed: checked 12353 }; 12354 if (checked && !prefersReducedMotion) { 12355 setExiting(true); 12356 } else { 12357 updateTask(updatedTask, type); 12358 } 12359 } 12360 12361 // When the CSS transition finishes, dispatch the real “completed = true” 12362 function handleTransitionEnd(e) { 12363 // only fire once for the exit: 12364 if (e.propertyName === "opacity" && exiting) { 12365 updateTask({ 12366 ...task, 12367 completed: true 12368 }, type); 12369 setExiting(false); 12370 } 12371 } 12372 function handleSave(newValue) { 12373 const trimmedTask = newValue.trimEnd(); 12374 if (trimmedTask && trimmedTask !== task.value) { 12375 updateTask({ 12376 ...task, 12377 value: newValue, 12378 isUrl: isValidUrl(trimmedTask) 12379 }, type); 12380 setIsEditing(false); 12381 } 12382 } 12383 function handleDelete() { 12384 deleteTask(task, type); 12385 } 12386 const taskLabel = task.isUrl ? /*#__PURE__*/external_React_default().createElement("a", { 12387 href: task.value, 12388 rel: "noopener noreferrer", 12389 target: "_blank", 12390 className: "task-label", 12391 title: task.value 12392 }, task.value) : /*#__PURE__*/external_React_default().createElement("label", { 12393 className: "task-label", 12394 title: task.value, 12395 htmlFor: `task-${task.id}`, 12396 onClick: () => setIsEditing(true) 12397 }, task.value); 12398 return /*#__PURE__*/external_React_default().createElement("div", { 12399 className: `task-item task-type-${type} ${exiting ? " exiting" : ""}`, 12400 id: task.id, 12401 key: task.id, 12402 onTransitionEnd: handleTransitionEnd 12403 }, /*#__PURE__*/external_React_default().createElement("div", { 12404 className: "checkbox-wrapper", 12405 key: isEditing 12406 }, /*#__PURE__*/external_React_default().createElement("input", { 12407 type: "checkbox", 12408 onChange: handleCheckboxChange, 12409 checked: task.completed || exiting, 12410 id: `task-${task.id}` 12411 }), isCompleted ? taskLabel : /*#__PURE__*/external_React_default().createElement(EditableText, { 12412 isEditing: isEditing, 12413 setIsEditing: setIsEditing, 12414 value: task.value, 12415 onSave: handleSave, 12416 type: "task" 12417 }, taskLabel)), /*#__PURE__*/external_React_default().createElement("moz-button", { 12418 iconSrc: "chrome://global/skin/icons/more.svg", 12419 menuId: `panel-task-${task.id}`, 12420 type: "ghost" 12421 }), /*#__PURE__*/external_React_default().createElement("panel-list", { 12422 id: `panel-task-${task.id}` 12423 }, !isCompleted && /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, task.isUrl && /*#__PURE__*/external_React_default().createElement("panel-item", { 12424 "data-l10n-id": "newtab-widget-lists-input-menu-open-link", 12425 onClick: () => window.open(task.value, "_blank", "noopener") 12426 }), /*#__PURE__*/external_React_default().createElement("panel-item", Lists_extends({}, isFirst ? { 12427 disabled: true 12428 } : {}, { 12429 onClick: () => moveTask(task, "up"), 12430 "data-l10n-id": "newtab-widget-lists-input-menu-move-up" 12431 })), /*#__PURE__*/external_React_default().createElement("panel-item", Lists_extends({}, isLast ? { 12432 disabled: true 12433 } : {}, { 12434 onClick: () => moveTask(task, "down"), 12435 "data-l10n-id": "newtab-widget-lists-input-menu-move-down" 12436 })), /*#__PURE__*/external_React_default().createElement("panel-item", { 12437 "data-l10n-id": "newtab-widget-lists-input-menu-edit", 12438 className: "edit-item", 12439 onClick: () => setIsEditing(true) 12440 })), /*#__PURE__*/external_React_default().createElement("panel-item", { 12441 "data-l10n-id": "newtab-widget-lists-input-menu-delete", 12442 className: "delete-item", 12443 onClick: handleDelete 12444 }))); 12445 } 12446 function EditableText({ 12447 value, 12448 isEditing, 12449 setIsEditing, 12450 onSave, 12451 onCancel, 12452 children, 12453 type, 12454 dataL10nId = null, 12455 maxLength = 100 12456 }) { 12457 const [tempValue, setTempValue] = (0,external_React_namespaceObject.useState)(value); 12458 const inputRef = (0,external_React_namespaceObject.useRef)(null); 12459 12460 // True if tempValue is empty, null/undefined, or only whitespace 12461 const showPlaceholder = (tempValue ?? "").trim() === ""; 12462 (0,external_React_namespaceObject.useEffect)(() => { 12463 if (isEditing) { 12464 inputRef.current?.focus(); 12465 } else { 12466 setTempValue(value); 12467 } 12468 }, [isEditing, value]); 12469 function handleKeyDown(e) { 12470 if (e.key === "Enter") { 12471 onSave(tempValue.trim()); 12472 setIsEditing(false); 12473 } else if (e.key === "Escape") { 12474 setIsEditing(false); 12475 setTempValue(value); 12476 onCancel?.(); 12477 } 12478 } 12479 function handleOnBlur() { 12480 onSave(tempValue.trim()); 12481 setIsEditing(false); 12482 } 12483 return isEditing ? /*#__PURE__*/external_React_default().createElement("input", Lists_extends({ 12484 className: `edit-${type}`, 12485 ref: inputRef, 12486 type: "text", 12487 value: tempValue, 12488 maxLength: maxLength, 12489 onChange: event => setTempValue(event.target.value), 12490 onBlur: handleOnBlur, 12491 onKeyDown: handleKeyDown 12492 // Note that if a user has a custom name set, it will override the placeholder 12493 }, showPlaceholder && dataL10nId ? { 12494 "data-l10n-id": dataL10nId 12495 } : {})) : [children]; 12496 } 12497 12498 ;// CONCATENATED MODULE: ./content-src/components/Widgets/FocusTimer/FocusTimer.jsx 12499 function FocusTimer_extends() { return FocusTimer_extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, FocusTimer_extends.apply(null, arguments); } 12500 /* This Source Code Form is subject to the terms of the Mozilla Public 12501 * License, v. 2.0. If a copy of the MPL was not distributed with this 12502 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 12503 12504 12505 12506 12507 12508 const FocusTimer_USER_ACTION_TYPES = { 12509 TIMER_SET: "timer_set", 12510 TIMER_PLAY: "timer_play", 12511 TIMER_PAUSE: "timer_pause", 12512 TIMER_RESET: "timer_reset", 12513 TIMER_END: "timer_end", 12514 TIMER_TOGGLE_FOCUS: "timer_toggle_focus", 12515 TIMER_TOGGLE_BREAK: "timer_toggle_break" 12516 }; 12517 12518 /** 12519 * Calculates the remaining time (in seconds) by subtracting elapsed time from the original duration 12520 * 12521 * @param duration 12522 * @param start 12523 * @returns int 12524 */ 12525 const calculateTimeRemaining = (duration, start) => { 12526 const currentTime = Math.floor(Date.now() / 1000); 12527 12528 // Subtract the elapsed time from initial duration to get time remaining in the timer 12529 return Math.max(duration - (currentTime - start), 0); 12530 }; 12531 12532 /** 12533 * Converts a number of seconds into a zero-padded MM:SS time string 12534 * 12535 * @param seconds 12536 * @returns string 12537 */ 12538 const formatTime = seconds => { 12539 const minutes = Math.floor(seconds / 60).toString().padStart(2, "0"); 12540 const secs = (seconds % 60).toString().padStart(2, "0"); 12541 return `${minutes}:${secs}`; 12542 }; 12543 12544 /** 12545 * Validates that the inputs in the timer only allow numerical digits (0-9) 12546 * 12547 * @param input - The character being input 12548 * @returns boolean - true if valid numeric input, false otherwise 12549 */ 12550 const isNumericValue = input => { 12551 // Check for null/undefined input or non-numeric characters 12552 return input && /^\d+$/.test(input); 12553 }; 12554 12555 /** 12556 * Validates if adding a new digit would exceed the 2-character limit 12557 * 12558 * @param currentValue - The current value in the field 12559 * @returns boolean - true if at 2-character limit, false otherwise 12560 */ 12561 const isAtMaxLength = currentValue => { 12562 return currentValue.length >= 2; 12563 }; 12564 12565 /** 12566 * Converts a polar coordinate (angle on circle) into a percentage-based [x,y] position for clip-path 12567 * 12568 * @param cx 12569 * @param cy 12570 * @param radius 12571 * @param angle 12572 * @returns string 12573 */ 12574 const polarToPercent = (cx, cy, radius, angle) => { 12575 const rad = (angle - 90) * Math.PI / 180; 12576 const x = cx + radius * Math.cos(rad); 12577 const y = cy + radius * Math.sin(rad); 12578 return `${x}% ${y}%`; 12579 }; 12580 12581 /** 12582 * Generates a clip-path polygon string that represents a pie slice from 0 degrees 12583 * to the current progress angle 12584 * 12585 * @returns string 12586 * @param progress 12587 */ 12588 const getClipPath = progress => { 12589 const cx = 50; 12590 const cy = 50; 12591 const radius = 50; 12592 // Show some progress right at the start - 6 degrees is just enough to paint a dot once the timer is ticking 12593 const angle = progress > 0 ? Math.max(progress * 360, 6) : 0; 12594 const points = [`50% 50%`]; 12595 for (let a = 0; a <= angle; a += 2) { 12596 points.push(polarToPercent(cx, cy, radius, a)); 12597 } 12598 return `polygon(${points.join(", ")})`; 12599 }; 12600 const FocusTimer = ({ 12601 dispatch, 12602 handleUserInteraction, 12603 isMaximized 12604 }) => { 12605 const [timeLeft, setTimeLeft] = (0,external_React_namespaceObject.useState)(0); 12606 // calculated value for the progress circle; 1 = 100% 12607 const [progress, setProgress] = (0,external_React_namespaceObject.useState)(0); 12608 const activeMinutesRef = (0,external_React_namespaceObject.useRef)(null); 12609 const activeSecondsRef = (0,external_React_namespaceObject.useRef)(null); 12610 const arcRef = (0,external_React_namespaceObject.useRef)(null); 12611 const timerType = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.TimerWidget.timerType); 12612 const timerData = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.TimerWidget); 12613 const { 12614 duration, 12615 initialDuration, 12616 startTime, 12617 isRunning 12618 } = timerData[timerType]; 12619 const initialTimerDuration = timerData[timerType].initialDuration; 12620 const handleTimerInteraction = (0,external_React_namespaceObject.useCallback)(() => handleUserInteraction("focusTimer"), [handleUserInteraction]); 12621 const handleIntersection = (0,external_React_namespaceObject.useCallback)(() => { 12622 dispatch(actionCreators.AlsoToMain({ 12623 type: actionTypes.WIDGETS_TIMER_USER_IMPRESSION 12624 })); 12625 }, [dispatch]); 12626 const timerRef = useIntersectionObserver(handleIntersection); 12627 const resetProgressCircle = (0,external_React_namespaceObject.useCallback)(() => { 12628 if (arcRef?.current) { 12629 arcRef.current.style.clipPath = "polygon(50% 50%)"; 12630 arcRef.current.style.webkitClipPath = "polygon(50% 50%)"; 12631 } 12632 setProgress(0); 12633 handleTimerInteraction(); 12634 }, [arcRef, handleTimerInteraction]); 12635 const prefs = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Prefs.values); 12636 const showSystemNotifications = prefs["widgets.focusTimer.showSystemNotifications"]; 12637 (0,external_React_namespaceObject.useEffect)(() => { 12638 // resets default values after timer ends 12639 let interval; 12640 let hasReachedZero = false; 12641 if (isRunning && duration > 0) { 12642 interval = setInterval(() => { 12643 const currentTime = Math.floor(Date.now() / 1000); 12644 const elapsed = currentTime - startTime; 12645 const remaining = calculateTimeRemaining(duration, startTime); 12646 12647 // using setTimeLeft to trigger a re-render of the component to show live countdown each second 12648 setTimeLeft(remaining); 12649 setProgress((initialDuration - remaining) / initialDuration); 12650 if (elapsed >= duration && hasReachedZero) { 12651 clearInterval(interval); 12652 (0,external_ReactRedux_namespaceObject.batch)(() => { 12653 dispatch(actionCreators.AlsoToMain({ 12654 type: actionTypes.WIDGETS_TIMER_END, 12655 data: { 12656 timerType, 12657 duration: initialTimerDuration, 12658 initialDuration: initialTimerDuration 12659 } 12660 })); 12661 dispatch(actionCreators.OnlyToMain({ 12662 type: actionTypes.WIDGETS_TIMER_USER_EVENT, 12663 data: { 12664 userAction: FocusTimer_USER_ACTION_TYPES.TIMER_END 12665 } 12666 })); 12667 }); 12668 12669 // animate the progress circle to turn solid green 12670 setProgress(1); 12671 12672 // More transitions after a delay to allow the animation above to complete 12673 setTimeout(() => { 12674 // progress circle goes back to default grey 12675 resetProgressCircle(); 12676 12677 // There's more to see! 12678 setTimeout(() => { 12679 // switch over to the other timer type 12680 // eslint-disable-next-line max-nested-callbacks 12681 (0,external_ReactRedux_namespaceObject.batch)(() => { 12682 dispatch(actionCreators.AlsoToMain({ 12683 type: actionTypes.WIDGETS_TIMER_SET_TYPE, 12684 data: { 12685 timerType: timerType === "focus" ? "break" : "focus" 12686 } 12687 })); 12688 dispatch(actionCreators.OnlyToMain({ 12689 type: actionTypes.WIDGETS_TIMER_USER_EVENT, 12690 data: { 12691 userAction: timerType === "focus" ? FocusTimer_USER_ACTION_TYPES.TIMER_TOGGLE_BREAK : FocusTimer_USER_ACTION_TYPES.TIMER_TOGGLE_FOCUS 12692 } 12693 })); 12694 }); 12695 }, 500); 12696 }, 1000); 12697 } else if (elapsed >= duration) { 12698 hasReachedZero = true; 12699 } 12700 }, 1000); 12701 } 12702 12703 // Shows the correct live time in the UI whenever the timer state changes 12704 const newTime = isRunning ? calculateTimeRemaining(duration, startTime) : duration; 12705 setTimeLeft(newTime); 12706 12707 // Set progress for paused timers (handles page load and timer type toggling) 12708 if (!isRunning && duration < initialDuration) { 12709 // Show previously elapsed time 12710 setProgress((initialDuration - duration) / initialDuration); 12711 } else if (!isRunning) { 12712 // Reset progress for fresh timers 12713 setProgress(0); 12714 } 12715 return () => clearInterval(interval); 12716 }, [isRunning, startTime, duration, initialDuration, dispatch, resetProgressCircle, timerType, initialTimerDuration]); 12717 12718 // Update the clip-path of the gradient circle to match the current progress value 12719 (0,external_React_namespaceObject.useEffect)(() => { 12720 if (arcRef?.current) { 12721 // Only set clip-path if current timer has been started or is running 12722 if (progress > 0 || isRunning) { 12723 arcRef.current.style.clipPath = getClipPath(progress); 12724 } else { 12725 arcRef.current.style.clipPath = ""; 12726 } 12727 } 12728 }, [progress, isRunning]); 12729 12730 // set timer function 12731 const setTimerDuration = () => { 12732 const minutesEl = activeMinutesRef.current; 12733 const secondsEl = activeSecondsRef.current; 12734 const minutesValue = minutesEl.innerText.trim() || "0"; 12735 const secondsValue = secondsEl.innerText.trim() || "0"; 12736 let minutes = parseInt(minutesValue || "0", 10); 12737 let seconds = parseInt(secondsValue || "0", 10); 12738 12739 // Set a limit of 99 minutes 12740 minutes = Math.min(minutes, 99); 12741 // Set a limit of 59 seconds 12742 seconds = Math.min(seconds, 59); 12743 const totalSeconds = minutes * 60 + seconds; 12744 if (!Number.isNaN(totalSeconds) && totalSeconds > 0 && totalSeconds !== duration) { 12745 (0,external_ReactRedux_namespaceObject.batch)(() => { 12746 dispatch(actionCreators.AlsoToMain({ 12747 type: actionTypes.WIDGETS_TIMER_SET_DURATION, 12748 data: { 12749 timerType, 12750 duration: totalSeconds 12751 } 12752 })); 12753 dispatch(actionCreators.OnlyToMain({ 12754 type: actionTypes.WIDGETS_TIMER_USER_EVENT, 12755 data: { 12756 userAction: FocusTimer_USER_ACTION_TYPES.TIMER_SET 12757 } 12758 })); 12759 }); 12760 } 12761 handleTimerInteraction(); 12762 }; 12763 12764 // Pause timer function 12765 const toggleTimer = () => { 12766 if (!isRunning && duration > 0) { 12767 (0,external_ReactRedux_namespaceObject.batch)(() => { 12768 dispatch(actionCreators.AlsoToMain({ 12769 type: actionTypes.WIDGETS_TIMER_PLAY, 12770 data: { 12771 timerType 12772 } 12773 })); 12774 dispatch(actionCreators.OnlyToMain({ 12775 type: actionTypes.WIDGETS_TIMER_USER_EVENT, 12776 data: { 12777 userAction: FocusTimer_USER_ACTION_TYPES.TIMER_PLAY 12778 } 12779 })); 12780 }); 12781 } else if (isRunning) { 12782 // calculated to get the new baseline of the timer when it starts or resumes 12783 const remaining = calculateTimeRemaining(duration, startTime); 12784 (0,external_ReactRedux_namespaceObject.batch)(() => { 12785 dispatch(actionCreators.AlsoToMain({ 12786 type: actionTypes.WIDGETS_TIMER_PAUSE, 12787 data: { 12788 timerType, 12789 duration: remaining 12790 } 12791 })); 12792 dispatch(actionCreators.OnlyToMain({ 12793 type: actionTypes.WIDGETS_TIMER_USER_EVENT, 12794 data: { 12795 userAction: FocusTimer_USER_ACTION_TYPES.TIMER_PAUSE 12796 } 12797 })); 12798 }); 12799 } 12800 handleTimerInteraction(); 12801 }; 12802 12803 // reset timer function 12804 const resetTimer = () => { 12805 (0,external_ReactRedux_namespaceObject.batch)(() => { 12806 dispatch(actionCreators.AlsoToMain({ 12807 type: actionTypes.WIDGETS_TIMER_RESET, 12808 data: { 12809 timerType, 12810 duration: initialTimerDuration, 12811 initialDuration: initialTimerDuration 12812 } 12813 })); 12814 dispatch(actionCreators.OnlyToMain({ 12815 type: actionTypes.WIDGETS_TIMER_USER_EVENT, 12816 data: { 12817 userAction: FocusTimer_USER_ACTION_TYPES.TIMER_RESET 12818 } 12819 })); 12820 }); 12821 12822 // Reset progress value and gradient arc on the progress circle 12823 resetProgressCircle(); 12824 handleTimerInteraction(); 12825 }; 12826 12827 // Toggles between "focus" and "break" timer types 12828 const toggleType = type => { 12829 const oldTypeRemaining = calculateTimeRemaining(duration, startTime); 12830 (0,external_ReactRedux_namespaceObject.batch)(() => { 12831 // The type we are toggling away from automatically pauses 12832 dispatch(actionCreators.AlsoToMain({ 12833 type: actionTypes.WIDGETS_TIMER_PAUSE, 12834 data: { 12835 timerType, 12836 duration: oldTypeRemaining 12837 } 12838 })); 12839 dispatch(actionCreators.OnlyToMain({ 12840 type: actionTypes.WIDGETS_TIMER_USER_EVENT, 12841 data: { 12842 userAction: FocusTimer_USER_ACTION_TYPES.TIMER_PAUSE 12843 } 12844 })); 12845 12846 // Sets the current timer type so it persists when opening a new tab 12847 dispatch(actionCreators.AlsoToMain({ 12848 type: actionTypes.WIDGETS_TIMER_SET_TYPE, 12849 data: { 12850 timerType: type 12851 } 12852 })); 12853 dispatch(actionCreators.OnlyToMain({ 12854 type: actionTypes.WIDGETS_TIMER_USER_EVENT, 12855 data: { 12856 userAction: type === "focus" ? FocusTimer_USER_ACTION_TYPES.TIMER_TOGGLE_FOCUS : FocusTimer_USER_ACTION_TYPES.TIMER_TOGGLE_BREAK 12857 } 12858 })); 12859 }); 12860 handleTimerInteraction(); 12861 }; 12862 const handleKeyDown = e => { 12863 if (e.key === "Enter") { 12864 e.preventDefault(); 12865 setTimerDuration(e); 12866 handleTimerInteraction(); 12867 } 12868 if (e.key === "Tab") { 12869 setTimerDuration(e); 12870 handleTimerInteraction(); 12871 } 12872 }; 12873 const handleBeforeInput = e => { 12874 const input = e.data; 12875 const values = e.target.innerText.trim(); 12876 12877 // only allow numerical digits 0–9 for time input 12878 if (!isNumericValue(input)) { 12879 e.preventDefault(); 12880 return; 12881 } 12882 const selection = window.getSelection(); 12883 const selectedText = selection.toString(); 12884 12885 // if entire value is selected, replace it with the new input 12886 if (selectedText === values) { 12887 e.preventDefault(); // prevent default typing 12888 e.target.innerText = input; 12889 12890 // Places the caret at the end of the content-editable text 12891 // This is a known problem with content-editable where the caret 12892 const range = document.createRange(); 12893 range.selectNodeContents(e.target); 12894 range.collapse(false); 12895 const sel = window.getSelection(); 12896 sel.removeAllRanges(); 12897 sel.addRange(range); 12898 return; 12899 } 12900 12901 // only allow 2 values each for minutes and seconds 12902 if (isAtMaxLength(values)) { 12903 e.preventDefault(); 12904 } 12905 }; 12906 const handleFocus = e => { 12907 if (isRunning) { 12908 // calculated to get the new baseline of the timer when it starts or resumes 12909 const remaining = calculateTimeRemaining(duration, startTime); 12910 (0,external_ReactRedux_namespaceObject.batch)(() => { 12911 dispatch(actionCreators.AlsoToMain({ 12912 type: actionTypes.WIDGETS_TIMER_PAUSE, 12913 data: { 12914 timerType, 12915 duration: remaining 12916 } 12917 })); 12918 dispatch(actionCreators.OnlyToMain({ 12919 type: actionTypes.WIDGETS_TIMER_USER_EVENT, 12920 data: { 12921 userAction: FocusTimer_USER_ACTION_TYPES.TIMER_PAUSE 12922 } 12923 })); 12924 }); 12925 } 12926 12927 // highlight entire text when focused on the time. 12928 // this makes it easier to input the new time instead of backspacing 12929 const el = e.target; 12930 if (document.createRange && window.getSelection) { 12931 const range = document.createRange(); 12932 range.selectNodeContents(el); 12933 const sel = window.getSelection(); 12934 sel.removeAllRanges(); 12935 sel.addRange(range); 12936 } 12937 }; 12938 function handleLearnMore() { 12939 dispatch(actionCreators.OnlyToMain({ 12940 type: actionTypes.OPEN_LINK, 12941 data: { 12942 url: "https://support.mozilla.org/kb/firefox-new-tab-widgets" 12943 } 12944 })); 12945 handleTimerInteraction(); 12946 } 12947 function handlePrefUpdate(prefName, prefValue) { 12948 dispatch(actionCreators.OnlyToMain({ 12949 type: actionTypes.SET_PREF, 12950 data: { 12951 name: prefName, 12952 value: prefValue 12953 } 12954 })); 12955 handleTimerInteraction(); 12956 } 12957 return timerData ? /*#__PURE__*/external_React_default().createElement("article", { 12958 className: `focus-timer ${isMaximized ? "is-maximized" : ""}`, 12959 ref: el => { 12960 timerRef.current = [el]; 12961 } 12962 }, /*#__PURE__*/external_React_default().createElement("div", { 12963 className: "newtab-widget-timer-notification-title-wrapper" 12964 }, /*#__PURE__*/external_React_default().createElement("h3", { 12965 "data-l10n-id": "newtab-widget-timer-notification-title" 12966 }), /*#__PURE__*/external_React_default().createElement("div", { 12967 className: "focus-timer-context-menu-wrapper" 12968 }, /*#__PURE__*/external_React_default().createElement("moz-button", { 12969 className: "focus-timer-context-menu-button", 12970 iconSrc: "chrome://global/skin/icons/more.svg", 12971 menuId: "focus-timer-context-menu", 12972 type: "ghost" 12973 }), /*#__PURE__*/external_React_default().createElement("panel-list", { 12974 id: "focus-timer-context-menu" 12975 }, /*#__PURE__*/external_React_default().createElement("panel-item", { 12976 "data-l10n-id": showSystemNotifications ? "newtab-widget-timer-menu-notifications" : "newtab-widget-timer-menu-notifications-on", 12977 onClick: () => { 12978 handlePrefUpdate("widgets.focusTimer.showSystemNotifications", !showSystemNotifications); 12979 } 12980 }), /*#__PURE__*/external_React_default().createElement("panel-item", { 12981 "data-l10n-id": "newtab-widget-timer-menu-hide", 12982 onClick: () => { 12983 handlePrefUpdate("widgets.focusTimer.enabled", false); 12984 } 12985 }), /*#__PURE__*/external_React_default().createElement("panel-item", { 12986 "data-l10n-id": "newtab-widget-timer-menu-learn-more", 12987 onClick: handleLearnMore 12988 })))), /*#__PURE__*/external_React_default().createElement("div", { 12989 className: "focus-timer-tabs" 12990 }, /*#__PURE__*/external_React_default().createElement("div", { 12991 className: "focus-timer-tabs-buttons" 12992 }, /*#__PURE__*/external_React_default().createElement("moz-button", { 12993 type: timerType === "focus" ? "default" : "ghost", 12994 "data-l10n-id": "newtab-widget-timer-mode-focus", 12995 size: "small", 12996 onClick: () => toggleType("focus") 12997 }), /*#__PURE__*/external_React_default().createElement("moz-button", { 12998 type: timerType === "break" ? "default" : "ghost", 12999 "data-l10n-id": "newtab-widget-timer-mode-break", 13000 size: "small", 13001 onClick: () => toggleType("break") 13002 }))), /*#__PURE__*/external_React_default().createElement("div", { 13003 role: "progress", 13004 className: `progress-circle-wrapper ${!showSystemNotifications && !timerData[timerType].isRunning ? "is-small" : ""}` 13005 }, /*#__PURE__*/external_React_default().createElement("div", { 13006 className: `progress-circle-background${timerType === "break" ? "-break" : ""}` 13007 }), /*#__PURE__*/external_React_default().createElement("div", { 13008 className: `progress-circle ${timerType === "focus" ? "focus-visible" : "focus-hidden"}`, 13009 ref: timerType === "focus" ? arcRef : null 13010 }), /*#__PURE__*/external_React_default().createElement("div", { 13011 className: `progress-circle ${timerType === "break" ? "break-visible" : "break-hidden"}`, 13012 ref: timerType === "break" ? arcRef : null 13013 }), /*#__PURE__*/external_React_default().createElement("div", { 13014 className: `progress-circle-complete${progress === 1 ? " visible" : ""}` 13015 }), /*#__PURE__*/external_React_default().createElement("div", { 13016 role: "timer", 13017 className: "progress-circle-label" 13018 }, /*#__PURE__*/external_React_default().createElement(EditableTimerFields, { 13019 minutesRef: activeMinutesRef, 13020 secondsRef: activeSecondsRef, 13021 onKeyDown: handleKeyDown, 13022 onBeforeInput: handleBeforeInput, 13023 onFocus: handleFocus, 13024 timeLeft: timeLeft, 13025 onBlur: () => setTimerDuration() 13026 }))), /*#__PURE__*/external_React_default().createElement("div", { 13027 className: "set-timer-controls-wrapper" 13028 }, /*#__PURE__*/external_React_default().createElement("div", { 13029 className: `focus-timer-controls timer-running` 13030 }, /*#__PURE__*/external_React_default().createElement("moz-button", FocusTimer_extends({}, !isRunning ? { 13031 type: "primary" 13032 } : {}, { 13033 iconsrc: `chrome://global/skin/media/${isRunning ? "pause" : "play"}-fill.svg`, 13034 "data-l10n-id": isRunning ? "newtab-widget-timer-label-pause" : "newtab-widget-timer-label-play", 13035 onClick: toggleTimer 13036 })), isRunning && /*#__PURE__*/external_React_default().createElement("moz-button", { 13037 type: "icon ghost", 13038 iconsrc: "chrome://newtab/content/data/content/assets/arrow-clockwise-16.svg", 13039 "data-l10n-id": "newtab-widget-timer-reset", 13040 onClick: resetTimer 13041 }))), !showSystemNotifications && !timerData[timerType].isRunning && /*#__PURE__*/external_React_default().createElement("p", { 13042 className: "timer-notification-status", 13043 "data-l10n-id": "newtab-widget-timer-notification-warning" 13044 })) : null; 13045 }; 13046 function EditableTimerFields({ 13047 minutesRef, 13048 secondsRef, 13049 tabIndex = 0, 13050 ...props 13051 }) { 13052 return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("span", { 13053 contentEditable: "true", 13054 ref: minutesRef, 13055 className: "timer-set-minutes", 13056 onKeyDown: props.onKeyDown, 13057 onBeforeInput: props.onBeforeInput, 13058 onFocus: props.onFocus, 13059 onBlur: props.onBlur, 13060 tabIndex: tabIndex 13061 }, formatTime(props.timeLeft).split(":")[0]), ":", /*#__PURE__*/external_React_default().createElement("span", { 13062 contentEditable: "true", 13063 ref: secondsRef, 13064 className: "timer-set-seconds", 13065 onKeyDown: props.onKeyDown, 13066 onBeforeInput: props.onBeforeInput, 13067 onFocus: props.onFocus, 13068 onBlur: props.onBlur, 13069 tabIndex: tabIndex 13070 }, formatTime(props.timeLeft).split(":")[1])); 13071 } 13072 ;// CONCATENATED MODULE: ./content-src/components/Widgets/WeatherForecast/WeatherForecast.jsx 13073 /* This Source Code Form is subject to the terms of the Mozilla Public 13074 * License, v. 2.0. If a copy of the MPL was not distributed with this 13075 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 13076 13077 13078 function WeatherForecast() { 13079 const prefs = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Prefs.values); 13080 const weatherData = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Weather); 13081 const WEATHER_SUGGESTION = weatherData.suggestions?.[0]; 13082 const showDetailedView = prefs["weather.display"] === "detailed"; 13083 if (!showDetailedView || !weatherData?.initialized) { 13084 return null; 13085 } 13086 return /*#__PURE__*/React.createElement("article", { 13087 className: "weather-forecast-widget" 13088 }, /*#__PURE__*/React.createElement("div", { 13089 className: "city-wrapper" 13090 }, /*#__PURE__*/React.createElement("h3", null, weatherData.locationData.city)), /*#__PURE__*/React.createElement("div", { 13091 className: "current-weather-wrapper" 13092 }, /*#__PURE__*/React.createElement("div", { 13093 className: "weather-icon-column" 13094 }, /*#__PURE__*/React.createElement("span", { 13095 className: `weather-icon iconId${WEATHER_SUGGESTION.current_conditions.icon_id}` 13096 })), /*#__PURE__*/React.createElement("div", { 13097 className: "weather-info-column" 13098 }, /*#__PURE__*/React.createElement("span", { 13099 className: "temperature-unit" 13100 }, WEATHER_SUGGESTION.current_conditions.temperature[prefs["weather.temperatureUnits"]], "\xB0", prefs["weather.temperatureUnits"]), /*#__PURE__*/React.createElement("span", { 13101 className: "temperature-description" 13102 }, WEATHER_SUGGESTION.current_conditions.summary)), /*#__PURE__*/React.createElement("div", { 13103 className: "high-low-column" 13104 }, /*#__PURE__*/React.createElement("span", { 13105 className: "high-temperature" 13106 }, /*#__PURE__*/React.createElement("span", { 13107 className: "arrow-icon arrow-up" 13108 }), WEATHER_SUGGESTION.forecast.high[prefs["weather.temperatureUnits"]], "\xB0"), /*#__PURE__*/React.createElement("span", { 13109 className: "low-temperature" 13110 }, /*#__PURE__*/React.createElement("span", { 13111 className: "arrow-icon arrow-down" 13112 }), WEATHER_SUGGESTION.forecast.low[prefs["weather.temperatureUnits"]], "\xB0"))), /*#__PURE__*/React.createElement("hr", null), /*#__PURE__*/React.createElement("div", { 13113 className: "forecast-row" 13114 }, /*#__PURE__*/React.createElement("p", { 13115 className: "today-forecast", 13116 "data-l10n-id": "newtab-weather-todays-forecast" 13117 }), /*#__PURE__*/React.createElement("ul", { 13118 className: "forecast-row-items" 13119 }, /*#__PURE__*/React.createElement("li", null, /*#__PURE__*/React.createElement("span", null, "80\xB0"), /*#__PURE__*/React.createElement("span", { 13120 className: `weather-icon iconId${WEATHER_SUGGESTION.current_conditions.icon_id}` 13121 }), /*#__PURE__*/React.createElement("span", null, "7:00")), /*#__PURE__*/React.createElement("li", null, /*#__PURE__*/React.createElement("span", null, "80\xB0"), /*#__PURE__*/React.createElement("span", { 13122 className: `weather-icon iconId${WEATHER_SUGGESTION.current_conditions.icon_id}` 13123 }), /*#__PURE__*/React.createElement("span", null, "7:00")), /*#__PURE__*/React.createElement("li", null, /*#__PURE__*/React.createElement("span", null, "80\xB0"), /*#__PURE__*/React.createElement("span", { 13124 className: `weather-icon iconId${WEATHER_SUGGESTION.current_conditions.icon_id}` 13125 }), /*#__PURE__*/React.createElement("span", null, "7:00")), /*#__PURE__*/React.createElement("li", null, /*#__PURE__*/React.createElement("span", null, "80\xB0"), /*#__PURE__*/React.createElement("span", { 13126 className: `weather-icon iconId${WEATHER_SUGGESTION.current_conditions.icon_id}` 13127 }), /*#__PURE__*/React.createElement("span", null, "7:00")), /*#__PURE__*/React.createElement("li", null, /*#__PURE__*/React.createElement("span", null, "80\xB0"), /*#__PURE__*/React.createElement("span", { 13128 className: `weather-icon iconId${WEATHER_SUGGESTION.current_conditions.icon_id}` 13129 }), /*#__PURE__*/React.createElement("span", null, "7:00")))), /*#__PURE__*/React.createElement("div", { 13130 className: "weather-forecast-footer" 13131 }, /*#__PURE__*/React.createElement("a", { 13132 href: "#", 13133 className: "full-forecast", 13134 "data-l10n-id": "newtab-weather-see-full-forecast" 13135 }), /*#__PURE__*/React.createElement("span", { 13136 className: "sponsored-text", 13137 "data-l10n-id": "newtab-weather-sponsored", 13138 "data-l10n-args": "{\"provider\": \"AccuWeather\xAE\"}" 13139 }))); 13140 } 13141 13142 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/FeatureHighlight/WidgetsFeatureHighlight.jsx 13143 /* This Source Code Form is subject to the terms of the Mozilla Public 13144 * License, v. 2.0. If a copy of the MPL was not distributed with this 13145 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 13146 13147 13148 13149 function WidgetsFeatureHighlight({ 13150 handleDismiss, 13151 handleBlock, 13152 dispatch 13153 }) { 13154 const { 13155 messageData 13156 } = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Messages); 13157 return /*#__PURE__*/React.createElement(FeatureHighlight, { 13158 position: "inset-inline-center inset-block-end", 13159 arrowPosition: "arrow-top-center", 13160 openedOverride: true, 13161 showButtonIcon: false, 13162 feature: messageData?.content?.feature, 13163 modalClassName: "widget-highlight-wrapper", 13164 message: /*#__PURE__*/React.createElement("div", { 13165 className: "widget-highlight" 13166 }, /*#__PURE__*/React.createElement("img", { 13167 src: "chrome://newtab/content/data/content/assets/widget-message.png", 13168 alt: "" 13169 }), /*#__PURE__*/React.createElement("h3", { 13170 "data-l10n-id": "newtab-widget-message-title" 13171 }), /*#__PURE__*/React.createElement("p", { 13172 "data-l10n-id": "newtab-widget-message-copy" 13173 })), 13174 dispatch: dispatch, 13175 dismissCallback: () => { 13176 handleDismiss(); 13177 handleBlock(); 13178 }, 13179 outsideClickCallback: handleDismiss 13180 }); 13181 } 13182 13183 ;// CONCATENATED MODULE: ./content-src/components/Widgets/Widgets.jsx 13184 /* This Source Code Form is subject to the terms of the Mozilla Public 13185 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 13186 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 13187 13188 13189 13190 13191 13192 13193 13194 13195 13196 const PREF_WIDGETS_LISTS_ENABLED = "widgets.lists.enabled"; 13197 const PREF_WIDGETS_SYSTEM_LISTS_ENABLED = "widgets.system.lists.enabled"; 13198 const PREF_WIDGETS_TIMER_ENABLED = "widgets.focusTimer.enabled"; 13199 const PREF_WIDGETS_SYSTEM_TIMER_ENABLED = "widgets.system.focusTimer.enabled"; 13200 const PREF_WIDGETS_WEATHER_FORECAST_ENABLED = "widgets.weatherForecast.enabled"; 13201 const PREF_WIDGETS_SYSTEM_WEATHER_FORECAST_ENABLED = "widgets.system.weatherForecast.enabled"; 13202 const PREF_WIDGETS_MAXIMIZED = "widgets.maximized"; 13203 const PREF_WIDGETS_SYSTEM_MAXIMIZED = "widgets.system.maximized"; 13204 13205 // resets timer to default values (exported for testing) 13206 // In practice, this logic runs inside a useEffect when 13207 // the timer widget is disabled (after the pref flips from true to false). 13208 // Because Enzyme tests cannot reliably simulate that pref update or trigger 13209 // the related useEffect, we expose this helper to at least just test the reset behavior instead 13210 13211 function resetTimerToDefaults(dispatch, timerType) { 13212 const originalTime = timerType === "focus" ? 1500 : 300; 13213 13214 // Reset both focus and break timers to their initial durations 13215 dispatch(actionCreators.AlsoToMain({ 13216 type: actionTypes.WIDGETS_TIMER_RESET, 13217 data: { 13218 timerType, 13219 duration: originalTime, 13220 initialDuration: originalTime 13221 } 13222 })); 13223 13224 // Set the timer type back to "focus" 13225 dispatch(actionCreators.AlsoToMain({ 13226 type: actionTypes.WIDGETS_TIMER_SET_TYPE, 13227 data: { 13228 timerType: "focus" 13229 } 13230 })); 13231 } 13232 function Widgets() { 13233 const prefs = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Prefs.values); 13234 const { 13235 messageData 13236 } = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Messages); 13237 const timerType = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.TimerWidget.timerType); 13238 const timerData = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.TimerWidget); 13239 const isMaximized = prefs[PREF_WIDGETS_MAXIMIZED]; 13240 const dispatch = (0,external_ReactRedux_namespaceObject.useDispatch)(); 13241 const nimbusListsEnabled = prefs.widgetsConfig?.listsEnabled; 13242 const nimbusTimerEnabled = prefs.widgetsConfig?.timerEnabled; 13243 const nimbusWeatherForecastEnabled = prefs.widgetsConfig?.weatherForecastEnabled; 13244 const nimbusListsTrainhopEnabled = prefs.trainhopConfig?.widgets?.listsEnabled; 13245 const nimbusTimerTrainhopEnabled = prefs.trainhopConfig?.widgets?.timerEnabled; 13246 const nimbusWeatherForecastTrainhopEnabled = prefs.trainhopConfig?.widgets?.weatherForecastEnabled; 13247 const listsEnabled = (nimbusListsTrainhopEnabled || nimbusListsEnabled || prefs[PREF_WIDGETS_SYSTEM_LISTS_ENABLED]) && prefs[PREF_WIDGETS_LISTS_ENABLED]; 13248 const timerEnabled = (nimbusTimerTrainhopEnabled || nimbusTimerEnabled || prefs[PREF_WIDGETS_SYSTEM_TIMER_ENABLED]) && prefs[PREF_WIDGETS_TIMER_ENABLED]; 13249 const weatherForecastEnabled = (nimbusWeatherForecastTrainhopEnabled || nimbusWeatherForecastEnabled || prefs[PREF_WIDGETS_SYSTEM_WEATHER_FORECAST_ENABLED]) && prefs[PREF_WIDGETS_WEATHER_FORECAST_ENABLED]; 13250 13251 // track previous timerEnabled state to detect when it becomes disabled 13252 const prevTimerEnabledRef = (0,external_React_namespaceObject.useRef)(timerEnabled); 13253 13254 // Reset timer when it becomes disabled 13255 (0,external_React_namespaceObject.useEffect)(() => { 13256 const wasTimerEnabled = prevTimerEnabledRef.current; 13257 const isTimerEnabled = timerEnabled; 13258 13259 // Only reset if timer was enabled and is now disabled 13260 if (wasTimerEnabled && !isTimerEnabled && timerData) { 13261 resetTimerToDefaults(dispatch, timerType); 13262 } 13263 13264 // Update the ref to track current state 13265 prevTimerEnabledRef.current = isTimerEnabled; 13266 }, [timerEnabled, timerData, dispatch, timerType]); 13267 13268 // Sends a dispatch to disable all widgets 13269 function handleHideAllWidgetsClick(e) { 13270 e.preventDefault(); 13271 (0,external_ReactRedux_namespaceObject.batch)(() => { 13272 dispatch(actionCreators.SetPref(PREF_WIDGETS_LISTS_ENABLED, false)); 13273 dispatch(actionCreators.SetPref(PREF_WIDGETS_TIMER_ENABLED, false)); 13274 }); 13275 } 13276 function handleHideAllWidgetsKeyDown(e) { 13277 if (e.key === "Enter" || e.key === " ") { 13278 e.preventDefault(); 13279 (0,external_ReactRedux_namespaceObject.batch)(() => { 13280 dispatch(actionCreators.SetPref(PREF_WIDGETS_LISTS_ENABLED, false)); 13281 dispatch(actionCreators.SetPref(PREF_WIDGETS_TIMER_ENABLED, false)); 13282 }); 13283 } 13284 } 13285 13286 // Toggles the maximized state of widgets 13287 function handleToggleMaximizeClick(e) { 13288 e.preventDefault(); 13289 dispatch(actionCreators.SetPref(PREF_WIDGETS_MAXIMIZED, !isMaximized)); 13290 } 13291 function handleToggleMaximizeKeyDown(e) { 13292 if (e.key === "Enter" || e.key === " ") { 13293 e.preventDefault(); 13294 dispatch(actionCreators.SetPref(PREF_WIDGETS_MAXIMIZED, !isMaximized)); 13295 } 13296 } 13297 function handleUserInteraction(widgetName) { 13298 const prefName = `widgets.${widgetName}.interaction`; 13299 const hasInteracted = prefs[prefName]; 13300 // we want to make sure that the value is a strict false (and that the property exists) 13301 if (hasInteracted === false) { 13302 dispatch(actionCreators.SetPref(prefName, true)); 13303 } 13304 } 13305 if (!(listsEnabled || timerEnabled || weatherForecastEnabled)) { 13306 return null; 13307 } 13308 return /*#__PURE__*/external_React_default().createElement("div", { 13309 className: "widgets-wrapper" 13310 }, /*#__PURE__*/external_React_default().createElement("div", { 13311 className: "widgets-section-container" 13312 }, /*#__PURE__*/external_React_default().createElement("div", { 13313 className: "widgets-title-container" 13314 }, /*#__PURE__*/external_React_default().createElement("h1", { 13315 "data-l10n-id": "newtab-widget-section-title" 13316 }), prefs[PREF_WIDGETS_SYSTEM_MAXIMIZED] && /*#__PURE__*/external_React_default().createElement("moz-button", { 13317 id: "toggle-widgets-size-button", 13318 type: "icon ghost", 13319 size: "small" 13320 // Toggle the icon and hover text 13321 , 13322 "data-l10n-id": isMaximized ? "newtab-widget-section-maximize" : "newtab-widget-section-minimize", 13323 iconsrc: `chrome://browser/skin/${isMaximized ? "fullscreen" : "fullscreen-exit"}.svg`, 13324 onClick: handleToggleMaximizeClick, 13325 onKeyDown: handleToggleMaximizeKeyDown 13326 }), /*#__PURE__*/external_React_default().createElement("moz-button", { 13327 id: "hide-all-widgets-button", 13328 type: "icon ghost", 13329 size: "small", 13330 "data-l10n-id": "newtab-widget-section-hide-all-button", 13331 iconsrc: "chrome://global/skin/icons/close.svg", 13332 onClick: handleHideAllWidgetsClick, 13333 onKeyDown: handleHideAllWidgetsKeyDown 13334 })), /*#__PURE__*/external_React_default().createElement("div", { 13335 className: `widgets-container ${isMaximized ? "is-maximized" : ""}` 13336 }, listsEnabled && /*#__PURE__*/external_React_default().createElement(Lists, { 13337 dispatch: dispatch, 13338 handleUserInteraction: handleUserInteraction, 13339 isMaximized: isMaximized 13340 }), timerEnabled && /*#__PURE__*/external_React_default().createElement(FocusTimer, { 13341 dispatch: dispatch, 13342 handleUserInteraction: handleUserInteraction, 13343 isMaximized: isMaximized 13344 }), weatherForecastEnabled && /*#__PURE__*/external_React_default().createElement(WeatherForecast, { 13345 dispatch: dispatch, 13346 handleUserInteraction: handleUserInteraction, 13347 isMaximized: isMaximized 13348 }))), messageData?.content?.messageType === "WidgetMessage" && /*#__PURE__*/external_React_default().createElement(MessageWrapper, { 13349 dispatch: dispatch 13350 }, /*#__PURE__*/external_React_default().createElement(WidgetsFeatureHighlight, { 13351 dispatch: dispatch 13352 }))); 13353 } 13354 13355 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx 13356 /* This Source Code Form is subject to the terms of the Mozilla Public 13357 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 13358 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 13359 13360 13361 13362 13363 13364 13365 13366 13367 13368 13369 13370 13371 13372 13373 13374 13375 const ALLOWED_CSS_URL_PREFIXES = ["chrome://", "resource://", "https://img-getpocket.cdn.mozilla.net/"]; 13376 const DUMMY_CSS_SELECTOR = "DUMMY#CSS.SELECTOR"; 13377 13378 /** 13379 * Validate a CSS declaration. The values are assumed to be normalized by CSSOM. 13380 */ 13381 function isAllowedCSS(property, value) { 13382 // Bug 1454823: INTERNAL properties, e.g., -moz-context-properties, are 13383 // exposed but their values aren't resulting in getting nothing. Fortunately, 13384 // we don't care about validating the values of the current set of properties. 13385 if (value === undefined) { 13386 return true; 13387 } 13388 13389 // Make sure all urls are of the allowed protocols/prefixes 13390 const urls = value.match(/url\("[^"]+"\)/g); 13391 return !urls || urls.every(url => ALLOWED_CSS_URL_PREFIXES.some(prefix => url.slice(5).startsWith(prefix))); 13392 } 13393 class _DiscoveryStreamBase extends (external_React_default()).PureComponent { 13394 constructor(props) { 13395 super(props); 13396 this.onStyleMount = this.onStyleMount.bind(this); 13397 } 13398 onStyleMount(style) { 13399 // Unmounting style gets rid of old styles, so nothing else to do 13400 if (!style) { 13401 return; 13402 } 13403 const { 13404 sheet 13405 } = style; 13406 const styles = JSON.parse(style.dataset.styles); 13407 styles.forEach((row, rowIndex) => { 13408 row.forEach((component, componentIndex) => { 13409 // Nothing to do without optional styles overrides 13410 if (!component) { 13411 return; 13412 } 13413 Object.entries(component).forEach(([selectors, declarations]) => { 13414 // Start with a dummy rule to validate declarations and selectors 13415 sheet.insertRule(`${DUMMY_CSS_SELECTOR} {}`); 13416 const [rule] = sheet.cssRules; 13417 13418 // Validate declarations and remove any offenders. CSSOM silently 13419 // discards invalid entries, so here we apply extra restrictions. 13420 rule.style = declarations; 13421 [...rule.style].forEach(property => { 13422 const value = rule.style[property]; 13423 if (!isAllowedCSS(property, value)) { 13424 console.error(`Bad CSS declaration ${property}: ${value}`); 13425 rule.style.removeProperty(property); 13426 } 13427 }); 13428 13429 // Set the actual desired selectors scoped to the component 13430 const prefix = `.ds-layout > .ds-column:nth-child(${rowIndex + 1}) .ds-column-grid > :nth-child(${componentIndex + 1})`; 13431 // NB: Splitting on "," doesn't work with strings with commas, but 13432 // we're okay with not supporting those selectors 13433 rule.selectorText = selectors.split(",").map(selector => prefix + ( 13434 // Assume :pseudo-classes are for component instead of descendant 13435 selector[0] === ":" ? "" : " ") + selector).join(","); 13436 13437 // CSSOM silently ignores bad selectors, so we'll be noisy instead 13438 if (rule.selectorText === DUMMY_CSS_SELECTOR) { 13439 console.error(`Bad CSS selector ${selectors}`); 13440 } 13441 }); 13442 }); 13443 }); 13444 } 13445 renderComponent(component) { 13446 switch (component.type) { 13447 case "Highlights": 13448 return /*#__PURE__*/external_React_default().createElement(Highlights, null); 13449 case "TopSites": 13450 return /*#__PURE__*/external_React_default().createElement("div", { 13451 className: "ds-top-sites" 13452 }, /*#__PURE__*/external_React_default().createElement(TopSites_TopSites, { 13453 isFixed: true, 13454 title: component.header?.title 13455 })); 13456 case "Message": 13457 return /*#__PURE__*/external_React_default().createElement(DSMessage, { 13458 title: component.header && component.header.title, 13459 subtitle: component.header && component.header.subtitle, 13460 link_text: component.header && component.header.link_text, 13461 link_url: component.header && component.header.link_url, 13462 icon: component.header && component.header.icon 13463 }); 13464 case "SectionTitle": 13465 return /*#__PURE__*/external_React_default().createElement(SectionTitle, { 13466 header: component.header 13467 }); 13468 case "Navigation": 13469 return /*#__PURE__*/external_React_default().createElement(Navigation, { 13470 dispatch: this.props.dispatch, 13471 links: component.properties.links, 13472 extraLinks: component.properties.extraLinks, 13473 alignment: component.properties.alignment, 13474 explore_topics: component.properties.explore_topics, 13475 header: component.header, 13476 locale: this.props.App.locale, 13477 newFooterSection: component.newFooterSection, 13478 privacyNoticeURL: component.properties.privacyNoticeURL 13479 }); 13480 case "CardGrid": 13481 { 13482 const sectionsEnabled = this.props.Prefs.values["discoverystream.sections.enabled"]; 13483 if (sectionsEnabled) { 13484 return /*#__PURE__*/external_React_default().createElement(CardSections, { 13485 feed: component.feed, 13486 data: component.data, 13487 dispatch: this.props.dispatch, 13488 type: component.type, 13489 firstVisibleTimestamp: this.props.firstVisibleTimestamp, 13490 ctaButtonSponsors: component.properties.ctaButtonSponsors, 13491 ctaButtonVariant: component.properties.ctaButtonVariant, 13492 placeholder: this.props.placeholder 13493 }); 13494 } 13495 return /*#__PURE__*/external_React_default().createElement(CardGrid, { 13496 title: component.header && component.header.title, 13497 data: component.data, 13498 feed: component.feed, 13499 widgets: component.widgets, 13500 type: component.type, 13501 dispatch: this.props.dispatch, 13502 items: component.properties.items, 13503 hybridLayout: component.properties.hybridLayout, 13504 hideCardBackground: component.properties.hideCardBackground, 13505 fourCardLayout: component.properties.fourCardLayout, 13506 compactGrid: component.properties.compactGrid, 13507 ctaButtonSponsors: component.properties.ctaButtonSponsors, 13508 ctaButtonVariant: component.properties.ctaButtonVariant, 13509 hideDescriptions: this.props.DiscoveryStream.hideDescriptions, 13510 firstVisibleTimestamp: this.props.firstVisibleTimestamp, 13511 spocPositions: component.spocs?.positions, 13512 placeholder: this.props.placeholder 13513 }); 13514 } 13515 case "HorizontalRule": 13516 return /*#__PURE__*/external_React_default().createElement(HorizontalRule, null); 13517 case "PrivacyLink": 13518 return /*#__PURE__*/external_React_default().createElement(PrivacyLink, { 13519 properties: component.properties 13520 }); 13521 case "Widgets": 13522 return /*#__PURE__*/external_React_default().createElement(Widgets, null); 13523 default: 13524 return /*#__PURE__*/external_React_default().createElement("div", null, component.type); 13525 } 13526 } 13527 renderStyles(styles) { 13528 // Use json string as both the key and styles to render so React knows when 13529 // to unmount and mount a new instance for new styles. 13530 const json = JSON.stringify(styles); 13531 return /*#__PURE__*/external_React_default().createElement("style", { 13532 key: json, 13533 "data-styles": json, 13534 ref: this.onStyleMount 13535 }); 13536 } 13537 render() { 13538 const { 13539 locale 13540 } = this.props; 13541 // Bug 1980459 - Note that selectLayoutRender acts as a selector that transforms layout data based on current 13542 // preferences and experiment flags. It runs after Redux state is populated but before render. 13543 // Components removed in selectLayoutRender (e.g., Widgets or TopSites) will not appear in the 13544 // layoutRender result, and therefore will not be rendered here regardless of logic below. 13545 13546 // Select layout renders data by adding spocs and position to recommendations 13547 const { 13548 layoutRender 13549 } = selectLayoutRender({ 13550 state: this.props.DiscoveryStream, 13551 prefs: this.props.Prefs.values, 13552 locale 13553 }); 13554 const sectionsEnabled = this.props.Prefs.values["discoverystream.sections.enabled"]; 13555 const { 13556 config 13557 } = this.props.DiscoveryStream; 13558 const topicSelectionEnabled = this.props.Prefs.values["discoverystream.topicSelection.enabled"]; 13559 const reportAdsEnabled = this.props.Prefs.values["discoverystream.reportAds.enabled"]; 13560 const spocsEnabled = this.props.Prefs.values["unifiedAds.spocs.enabled"]; 13561 13562 // Allow rendering without extracting special components 13563 if (!config.collapsible) { 13564 return this.renderLayout(layoutRender); 13565 } 13566 13567 // Find the first component of a type and remove it from layout 13568 const extractComponent = type => { 13569 for (const [rowIndex, row] of Object.entries(layoutRender)) { 13570 for (const [index, component] of Object.entries(row.components)) { 13571 if (component.type === type) { 13572 // Remove the row if it was the only component or the single item 13573 if (row.components.length === 1) { 13574 layoutRender.splice(rowIndex, 1); 13575 } else { 13576 row.components.splice(index, 1); 13577 } 13578 return component; 13579 } 13580 } 13581 } 13582 return null; 13583 }; 13584 13585 // Get "topstories" Section state for default values 13586 const topStories = this.props.Sections.find(s => s.id === "topstories"); 13587 if (!topStories) { 13588 return null; 13589 } 13590 13591 // Extract TopSites to render before the rest and Message to use for header 13592 const topSites = extractComponent("TopSites"); 13593 13594 // There are two ways to enable widgets: 13595 // Via `widgets.system.*` prefs or Nimbus experiment 13596 const widgetsNimbusTrainhopEnabled = this.props.Prefs.values.trainhopConfig?.widgets?.enabled; 13597 const widgetsNimbusEnabled = this.props.Prefs.values.widgetsConfig?.enabled; 13598 const widgetsSystemPrefsEnabled = this.props.Prefs.values["widgets.system.enabled"]; 13599 const widgets = widgetsNimbusTrainhopEnabled || widgetsNimbusEnabled || widgetsSystemPrefsEnabled; 13600 const message = extractComponent("Message") || { 13601 header: { 13602 link_text: topStories.learnMore.link.message, 13603 link_url: topStories.learnMore.link.href, 13604 title: topStories.title 13605 } 13606 }; 13607 const privacyLinkComponent = extractComponent("PrivacyLink"); 13608 let learnMore = { 13609 link: { 13610 href: message.header.link_url, 13611 message: message.header.link_text 13612 } 13613 }; 13614 let sectionTitle = message.header.title; 13615 let subTitle = ""; 13616 const { 13617 DiscoveryStream 13618 } = this.props; 13619 return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, (reportAdsEnabled && spocsEnabled || sectionsEnabled) && /*#__PURE__*/external_React_default().createElement(ReportContent, { 13620 spocs: DiscoveryStream.spocs 13621 }), topSites && this.renderLayout([{ 13622 width: 12, 13623 components: [topSites], 13624 sectionType: "topsites" 13625 }]), widgets && this.renderLayout([{ 13626 width: 12, 13627 components: [{ 13628 type: "Widgets" 13629 }], 13630 sectionType: "widgets" 13631 }]), !!layoutRender.length && /*#__PURE__*/external_React_default().createElement(CollapsibleSection, { 13632 className: "ds-layout", 13633 collapsed: topStories.pref.collapsed, 13634 dispatch: this.props.dispatch, 13635 id: topStories.id, 13636 isFixed: true, 13637 learnMore: learnMore, 13638 privacyNoticeURL: topStories.privacyNoticeURL, 13639 showPrefName: topStories.pref.feed, 13640 title: sectionTitle, 13641 subTitle: subTitle, 13642 mayHaveTopicsSelection: topicSelectionEnabled, 13643 sectionsEnabled: sectionsEnabled, 13644 eventSource: "CARDGRID" 13645 }, this.renderLayout(layoutRender)), this.renderLayout([{ 13646 width: 12, 13647 components: [{ 13648 type: "Highlights" 13649 }] 13650 }]), privacyLinkComponent && this.renderLayout([{ 13651 width: 12, 13652 components: [privacyLinkComponent] 13653 }])); 13654 } 13655 renderLayout(layoutRender) { 13656 const styles = []; 13657 let [data] = layoutRender; 13658 // Add helper class for topsites 13659 const sectionClass = data.sectionType ? `ds-layout-${data.sectionType}` : ""; 13660 return /*#__PURE__*/external_React_default().createElement("div", { 13661 className: `discovery-stream ds-layout ${sectionClass}` 13662 }, layoutRender.map((row, rowIndex) => /*#__PURE__*/external_React_default().createElement("div", { 13663 key: `row-${rowIndex}`, 13664 className: `ds-column ds-column-${row.width}` 13665 }, /*#__PURE__*/external_React_default().createElement("div", { 13666 className: "ds-column-grid" 13667 }, row.components.map((component, componentIndex) => { 13668 if (!component) { 13669 return null; 13670 } 13671 styles[rowIndex] = [...(styles[rowIndex] || []), component.styles]; 13672 return /*#__PURE__*/external_React_default().createElement("div", { 13673 key: `component-${componentIndex}` 13674 }, this.renderComponent(component, row.width)); 13675 })))), this.renderStyles(styles)); 13676 } 13677 } 13678 const DiscoveryStreamBase = (0,external_ReactRedux_namespaceObject.connect)(state => ({ 13679 DiscoveryStream: state.DiscoveryStream, 13680 Prefs: state.Prefs, 13681 Sections: state.Sections, 13682 document: globalThis.document, 13683 App: state.App 13684 }))(_DiscoveryStreamBase); 13685 ;// CONCATENATED MODULE: ./content-src/components/CustomizeMenu/SectionsMgmtPanel/SectionsMgmtPanel.jsx 13686 function SectionsMgmtPanel_extends() { return SectionsMgmtPanel_extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, SectionsMgmtPanel_extends.apply(null, arguments); } 13687 /* This Source Code Form is subject to the terms of the Mozilla Public 13688 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 13689 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 13690 13691 13692 13693 13694 // eslint-disable-next-line no-shadow 13695 13696 function SectionsMgmtPanel({ 13697 exitEventFired, 13698 pocketEnabled, 13699 onSubpanelToggle, 13700 togglePanel, 13701 showPanel 13702 }) { 13703 const arrowButtonRef = (0,external_React_namespaceObject.useRef)(null); 13704 const { 13705 sectionPersonalization 13706 } = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.DiscoveryStream); 13707 const layoutComponents = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.DiscoveryStream.layout[0].components); 13708 const sections = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.DiscoveryStream.feeds.data); 13709 const dispatch = (0,external_ReactRedux_namespaceObject.useDispatch)(); 13710 13711 // TODO: Wrap sectionsFeedName -> sectionsList logic in try...catch? 13712 let sectionsFeedName; 13713 const cardGridEntry = layoutComponents.find(item => item.type === "CardGrid"); 13714 if (cardGridEntry) { 13715 sectionsFeedName = cardGridEntry.feed.url; 13716 } 13717 let sectionsList; 13718 if (sectionsFeedName) { 13719 sectionsList = sections[sectionsFeedName].data.sections; 13720 } 13721 const [sectionsState, setSectionState] = (0,external_React_namespaceObject.useState)(sectionPersonalization); // State management with useState 13722 13723 let followedSectionsData = sectionsList.filter(item => sectionsState[item.sectionKey]?.isFollowed); 13724 let blockedSectionsData = sectionsList.filter(item => sectionsState[item.sectionKey]?.isBlocked); 13725 function updateCachedData() { 13726 // Reset cached followed/blocked list data while panel is open 13727 setSectionState(sectionPersonalization); 13728 followedSectionsData = sectionsList.filter(item => sectionsState[item.sectionKey]?.isFollowed); 13729 blockedSectionsData = sectionsList.filter(item => sectionsState[item.sectionKey]?.isBlocked); 13730 } 13731 const onFollowClick = (0,external_React_namespaceObject.useCallback)((sectionKey, receivedRank) => { 13732 dispatch(actionCreators.AlsoToMain({ 13733 type: actionTypes.SECTION_PERSONALIZATION_SET, 13734 data: { 13735 ...sectionPersonalization, 13736 [sectionKey]: { 13737 isFollowed: true, 13738 isBlocked: false, 13739 followedAt: new Date().toISOString() 13740 } 13741 } 13742 })); 13743 // Telemetry Event Dispatch 13744 dispatch(actionCreators.OnlyToMain({ 13745 type: "FOLLOW_SECTION", 13746 data: { 13747 section: sectionKey, 13748 section_position: receivedRank, 13749 event_source: "CUSTOMIZE_PANEL" 13750 } 13751 })); 13752 }, [dispatch, sectionPersonalization]); 13753 const onBlockClick = (0,external_React_namespaceObject.useCallback)((sectionKey, receivedRank) => { 13754 dispatch(actionCreators.AlsoToMain({ 13755 type: actionTypes.SECTION_PERSONALIZATION_SET, 13756 data: { 13757 ...sectionPersonalization, 13758 [sectionKey]: { 13759 isFollowed: false, 13760 isBlocked: true 13761 } 13762 } 13763 })); 13764 13765 // Telemetry Event Dispatch 13766 dispatch(actionCreators.OnlyToMain({ 13767 type: "BLOCK_SECTION", 13768 data: { 13769 section: sectionKey, 13770 section_position: receivedRank, 13771 event_source: "CUSTOMIZE_PANEL" 13772 } 13773 })); 13774 }, [dispatch, sectionPersonalization]); 13775 const onUnblockClick = (0,external_React_namespaceObject.useCallback)((sectionKey, receivedRank) => { 13776 const updatedSectionData = { 13777 ...sectionPersonalization 13778 }; 13779 delete updatedSectionData[sectionKey]; 13780 dispatch(actionCreators.AlsoToMain({ 13781 type: actionTypes.SECTION_PERSONALIZATION_SET, 13782 data: updatedSectionData 13783 })); 13784 // Telemetry Event Dispatch 13785 dispatch(actionCreators.OnlyToMain({ 13786 type: "UNBLOCK_SECTION", 13787 data: { 13788 section: sectionKey, 13789 section_position: receivedRank, 13790 event_source: "CUSTOMIZE_PANEL" 13791 } 13792 })); 13793 }, [dispatch, sectionPersonalization]); 13794 const onUnfollowClick = (0,external_React_namespaceObject.useCallback)((sectionKey, receivedRank) => { 13795 const updatedSectionData = { 13796 ...sectionPersonalization 13797 }; 13798 delete updatedSectionData[sectionKey]; 13799 dispatch(actionCreators.AlsoToMain({ 13800 type: actionTypes.SECTION_PERSONALIZATION_SET, 13801 data: updatedSectionData 13802 })); 13803 // Telemetry Event Dispatch 13804 dispatch(actionCreators.OnlyToMain({ 13805 type: "UNFOLLOW_SECTION", 13806 data: { 13807 section: sectionKey, 13808 section_position: receivedRank, 13809 event_source: "CUSTOMIZE_PANEL" 13810 } 13811 })); 13812 }, [dispatch, sectionPersonalization]); 13813 13814 // Close followed/blocked topic subpanel when parent menu is closed 13815 (0,external_React_namespaceObject.useEffect)(() => { 13816 if (exitEventFired && showPanel) { 13817 togglePanel(); 13818 } 13819 }, [exitEventFired, showPanel, togglePanel]); 13820 13821 // Notify parent menu when subpanel opens/closes 13822 (0,external_React_namespaceObject.useEffect)(() => { 13823 if (onSubpanelToggle) { 13824 onSubpanelToggle(showPanel); 13825 } 13826 }, [showPanel, onSubpanelToggle]); 13827 (0,external_React_namespaceObject.useEffect)(() => { 13828 if (showPanel) { 13829 updateCachedData(); 13830 } 13831 // eslint-disable-next-line react-hooks/exhaustive-deps 13832 }, [showPanel]); 13833 const handlePanelEntered = () => { 13834 arrowButtonRef.current?.focus(); 13835 }; 13836 const followedSectionsList = followedSectionsData.map(({ 13837 sectionKey, 13838 title, 13839 receivedRank 13840 }) => { 13841 const following = sectionPersonalization[sectionKey]?.isFollowed; 13842 return /*#__PURE__*/external_React_default().createElement("li", { 13843 key: sectionKey 13844 }, /*#__PURE__*/external_React_default().createElement("label", { 13845 htmlFor: `follow-topic-${sectionKey}` 13846 }, title), /*#__PURE__*/external_React_default().createElement("div", { 13847 className: following ? "section-follow following" : "section-follow" 13848 }, /*#__PURE__*/external_React_default().createElement("moz-button", { 13849 onClick: () => following ? onUnfollowClick(sectionKey, receivedRank) : onFollowClick(sectionKey, receivedRank), 13850 type: "default", 13851 index: receivedRank, 13852 section: sectionKey, 13853 id: `follow-topic-${sectionKey}` 13854 }, /*#__PURE__*/external_React_default().createElement("span", { 13855 className: "section-button-follow-text", 13856 "data-l10n-id": "newtab-section-follow-button" 13857 }), /*#__PURE__*/external_React_default().createElement("span", { 13858 className: "section-button-following-text", 13859 "data-l10n-id": "newtab-section-following-button" 13860 }), /*#__PURE__*/external_React_default().createElement("span", { 13861 className: "section-button-unfollow-text", 13862 "data-l10n-id": "newtab-section-unfollow-button" 13863 })))); 13864 }); 13865 const blockedSectionsList = blockedSectionsData.map(({ 13866 sectionKey, 13867 title, 13868 receivedRank 13869 }) => { 13870 const blocked = sectionPersonalization[sectionKey]?.isBlocked; 13871 return /*#__PURE__*/external_React_default().createElement("li", { 13872 key: sectionKey 13873 }, /*#__PURE__*/external_React_default().createElement("label", { 13874 htmlFor: `blocked-topic-${sectionKey}` 13875 }, title), /*#__PURE__*/external_React_default().createElement("div", { 13876 className: blocked ? "section-block blocked" : "section-block" 13877 }, /*#__PURE__*/external_React_default().createElement("moz-button", { 13878 onClick: () => blocked ? onUnblockClick(sectionKey, receivedRank) : onBlockClick(sectionKey, receivedRank), 13879 type: "default", 13880 index: receivedRank, 13881 section: sectionKey, 13882 id: `blocked-topic-${sectionKey}` 13883 }, /*#__PURE__*/external_React_default().createElement("span", { 13884 className: "section-button-block-text", 13885 "data-l10n-id": "newtab-section-block-button" 13886 }), /*#__PURE__*/external_React_default().createElement("span", { 13887 className: "section-button-blocked-text", 13888 "data-l10n-id": "newtab-section-blocked-button" 13889 }), /*#__PURE__*/external_React_default().createElement("span", { 13890 className: "section-button-unblock-text", 13891 "data-l10n-id": "newtab-section-unblock-button" 13892 })))); 13893 }); 13894 return /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement("moz-box-button", SectionsMgmtPanel_extends({ 13895 onClick: togglePanel, 13896 "data-l10n-id": "newtab-section-manage-topics-button-v2" 13897 }, !pocketEnabled ? { 13898 disabled: true 13899 } : {})), /*#__PURE__*/external_React_default().createElement(external_ReactTransitionGroup_namespaceObject.CSSTransition, { 13900 in: showPanel, 13901 timeout: 300, 13902 classNames: "sections-mgmt-panel", 13903 unmountOnExit: true, 13904 onEntered: handlePanelEntered 13905 }, /*#__PURE__*/external_React_default().createElement("div", { 13906 className: "sections-mgmt-panel" 13907 }, /*#__PURE__*/external_React_default().createElement("button", { 13908 ref: arrowButtonRef, 13909 className: "arrow-button", 13910 onClick: togglePanel 13911 }, /*#__PURE__*/external_React_default().createElement("h1", { 13912 "data-l10n-id": "newtab-section-mangage-topics-title" 13913 })), /*#__PURE__*/external_React_default().createElement("h3", { 13914 "data-l10n-id": "newtab-section-mangage-topics-followed-topics" 13915 }), followedSectionsData.length ? /*#__PURE__*/external_React_default().createElement("ul", { 13916 className: "topic-list" 13917 }, followedSectionsList) : /*#__PURE__*/external_React_default().createElement("span", { 13918 className: "topic-list-empty-state", 13919 "data-l10n-id": "newtab-section-mangage-topics-followed-topics-empty-state" 13920 }), /*#__PURE__*/external_React_default().createElement("h3", { 13921 "data-l10n-id": "newtab-section-mangage-topics-blocked-topics" 13922 }), blockedSectionsData.length ? /*#__PURE__*/external_React_default().createElement("ul", { 13923 className: "topic-list" 13924 }, blockedSectionsList) : /*#__PURE__*/external_React_default().createElement("span", { 13925 className: "topic-list-empty-state", 13926 "data-l10n-id": "newtab-section-mangage-topics-blocked-topics-empty-state" 13927 })))); 13928 } 13929 13930 ;// CONCATENATED MODULE: ./content-src/components/WallpaperCategories/WallpaperCategories.jsx 13931 function WallpaperCategories_extends() { return WallpaperCategories_extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, WallpaperCategories_extends.apply(null, arguments); } 13932 /* This Source Code Form is subject to the terms of the Mozilla Public 13933 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 13934 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 13935 13936 13937 13938 13939 // eslint-disable-next-line no-shadow 13940 13941 const PREF_WALLPAPER_UPLOADED_PREVIOUSLY = "newtabWallpapers.customWallpaper.uploadedPreviously"; 13942 const PREF_WALLPAPER_UPLOAD_MAX_FILE_SIZE = "newtabWallpapers.customWallpaper.fileSize"; 13943 const PREF_WALLPAPER_UPLOAD_MAX_FILE_SIZE_ENABLED = "newtabWallpapers.customWallpaper.fileSize.enabled"; 13944 13945 // Returns a function will not be continuously triggered when called. The 13946 // function will be triggered if called again after `wait` milliseconds. 13947 function debounce(func, wait) { 13948 let timer; 13949 return (...args) => { 13950 if (timer) { 13951 return; 13952 } 13953 let wakeUp = () => { 13954 timer = null; 13955 }; 13956 timer = setTimeout(wakeUp, wait); 13957 func.apply(this, args); 13958 }; 13959 } 13960 class _WallpaperCategories extends (external_React_default()).PureComponent { 13961 constructor(props) { 13962 super(props); 13963 this.handleColorInput = this.handleColorInput.bind(this); 13964 this.debouncedHandleChange = debounce(this.handleChange.bind(this), 999); 13965 this.handleChange = this.handleChange.bind(this); 13966 this.handleReset = this.handleReset.bind(this); 13967 this.handleCategory = this.handleCategory.bind(this); 13968 this.focusCategory = this.focusCategory.bind(this); 13969 this.handleUpload = this.handleUpload.bind(this); 13970 this.handleBack = this.handleBack.bind(this); 13971 this.handleWallpaperListEntered = this.handleWallpaperListEntered.bind(this); 13972 this.getRGBColors = this.getRGBColors.bind(this); 13973 this.prefersHighContrastQuery = null; 13974 this.prefersDarkQuery = null; 13975 this.categoryRef = []; // store references for wallpaper category list 13976 this.wallpaperRef = []; // store reference for wallpaper selection list 13977 this.arrowButtonRef = /*#__PURE__*/external_React_default().createRef(); // Used to focus arrow button when category opens 13978 this.customColorPickerRef = /*#__PURE__*/external_React_default().createRef(); // Used to determine contrast icon color for custom color picker 13979 this.customColorInput = /*#__PURE__*/external_React_default().createRef(); // Used to determine contrast icon color for custom color picker 13980 this.state = { 13981 activeCategory: null, 13982 activeCategoryFluentID: null, 13983 showColorPicker: false, 13984 inputType: "radio", 13985 activeId: null, 13986 customWallpaperErrorType: null, 13987 focusedCategoryIndex: 0 13988 }; 13989 } 13990 componentDidMount() { 13991 this.prefersDarkQuery = globalThis.matchMedia("(prefers-color-scheme: dark)"); 13992 } 13993 componentDidUpdate(prevProps) { 13994 // Walllpaper category subpanel should close when parent menu is closed 13995 if (this.props.exitEventFired && this.props.exitEventFired !== prevProps.exitEventFired) { 13996 this.handleBack(); 13997 } 13998 } 13999 handleColorInput(event) { 14000 let { 14001 id 14002 } = event.target; 14003 // Set ID to include hex value of custom color 14004 id = `solid-color-picker-${event.target.value}`; 14005 const rgbColors = this.getRGBColors(event.target.value); 14006 14007 // Set background color to custom color 14008 event.target.style.backgroundColor = `rgb(${rgbColors.toString()})`; 14009 if (this.customColorPickerRef.current) { 14010 const colorInputBackground = this.customColorPickerRef.current.children[0].style.backgroundColor; 14011 this.customColorPickerRef.current.style.backgroundColor = colorInputBackground; 14012 } 14013 14014 // Set icon color based on the selected color 14015 const isColorDark = this.isWallpaperColorDark(rgbColors); 14016 if (this.customColorPickerRef.current) { 14017 if (isColorDark) { 14018 this.customColorPickerRef.current.classList.add("is-dark"); 14019 } else { 14020 this.customColorPickerRef.current.classList.remove("is-dark"); 14021 } 14022 14023 // Remove any possible initial classes 14024 this.customColorPickerRef.current.classList.remove("custom-color-set", "custom-color-dark", "default-color-set"); 14025 } 14026 14027 // Setting this now so when we remove v1 we don't have to migrate v1 values. 14028 this.props.setPref("newtabWallpapers.wallpaper", id); 14029 } 14030 14031 // Note: There's a separate event (debouncedHandleChange) that fires the handleChange 14032 // event but is delayed so that it doesn't fire multiple events when a user 14033 // is selecting a custom color background 14034 handleChange(event) { 14035 let { 14036 id 14037 } = event.target; 14038 14039 // Set ID to include hex value of custom color 14040 if (id === "solid-color-picker") { 14041 id = `solid-color-picker-${event.target.value}`; 14042 } 14043 this.props.setPref("newtabWallpapers.wallpaper", id); 14044 const uploadedPreviously = this.props.Prefs.values[PREF_WALLPAPER_UPLOADED_PREVIOUSLY]; 14045 this.handleUserEvent(actionTypes.WALLPAPER_CLICK, { 14046 selected_wallpaper: id, 14047 had_previous_wallpaper: !!this.props.activeWallpaper, 14048 had_uploaded_previously: !!uploadedPreviously 14049 }); 14050 } 14051 focusCategory(focusIndex) { 14052 if (!this.categoryRef) { 14053 return; 14054 } 14055 const el = this.categoryRef[focusIndex]; 14056 if (el) { 14057 el.focus(); 14058 } 14059 } 14060 14061 // function implementing arrow navigation for wallpaper category selection 14062 handleCategoryKeyDown(event, category) { 14063 const getIndex = this.categoryRef.findIndex(cat => cat.id === category); 14064 if (getIndex === -1) { 14065 return; // prevents errors if wallpaper index isn't found when navigating with arrow keys 14066 } 14067 const isRTL = document.dir === "rtl"; // returns true if page language is right-to-left 14068 let eventKey = event.key; 14069 if (eventKey === "ArrowRight" || eventKey === "ArrowLeft") { 14070 if (isRTL) { 14071 eventKey = eventKey === "ArrowRight" ? "ArrowLeft" : "ArrowRight"; 14072 } 14073 } 14074 let nextIndex = getIndex; 14075 if (eventKey === "ArrowRight") { 14076 nextIndex = getIndex + 1 < this.categoryRef.length ? getIndex + 1 : getIndex; 14077 } else if (eventKey === "ArrowLeft") { 14078 nextIndex = getIndex - 1 >= 0 ? getIndex - 1 : getIndex; 14079 } 14080 this.setState({ 14081 focusedCategoryIndex: nextIndex 14082 }, () => this.focusCategory(nextIndex)); 14083 } 14084 14085 // function implementing arrow navigation for wallpaper selection 14086 handleWallpaperKeyDown(event, title) { 14087 if (event.key === "Tab") { 14088 if (event.shiftKey) { 14089 event.preventDefault(); 14090 this.arrowButtonRef.current?.focus(); 14091 } else { 14092 event.preventDefault(); // prevent tabbing within wallpaper selection. We should only be using the Tab key to tab between groups 14093 } 14094 return; 14095 } 14096 const isRTL = document.dir === "rtl"; // returns true if page language is right-to-left 14097 let eventKey = event.key; 14098 if (eventKey === "ArrowRight" || eventKey === "ArrowLeft") { 14099 if (isRTL) { 14100 eventKey = eventKey === "ArrowRight" ? "ArrowLeft" : "ArrowRight"; 14101 } 14102 } 14103 const getIndex = this.wallpaperRef.findIndex(wallpaper => wallpaper.id === title); 14104 if (getIndex === -1) { 14105 return; // prevents errors if wallpaper index isn't found when navigating with arrow keys 14106 } 14107 14108 // the set layout of columns per row for the wallpaper selection 14109 const columnCount = 3; 14110 let nextIndex = getIndex; 14111 if (eventKey === "ArrowRight") { 14112 nextIndex = getIndex + 1 < this.wallpaperRef.length ? getIndex + 1 : getIndex; 14113 } else if (eventKey === "ArrowLeft") { 14114 nextIndex = getIndex - 1 >= 0 ? getIndex - 1 : getIndex; 14115 } else if (eventKey === "ArrowDown") { 14116 nextIndex = getIndex + columnCount < this.wallpaperRef.length ? getIndex + columnCount : getIndex; 14117 } else if (eventKey === "ArrowUp") { 14118 nextIndex = getIndex - columnCount >= 0 ? getIndex - columnCount : getIndex; 14119 } 14120 this.wallpaperRef[nextIndex].tabIndex = 0; 14121 this.wallpaperRef[getIndex].tabIndex = -1; 14122 this.wallpaperRef[nextIndex].focus(); 14123 this.wallpaperRef[nextIndex].click(); 14124 } 14125 handleReset() { 14126 const uploadedPreviously = this.props.Prefs.values[PREF_WALLPAPER_UPLOADED_PREVIOUSLY]; 14127 const selectedWallpaper = this.props.Prefs.values["newtabWallpapers.wallpaper"]; 14128 14129 // If a custom wallpaper is set, remove it 14130 if (selectedWallpaper === "custom") { 14131 this.props.dispatch(actionCreators.OnlyToMain({ 14132 type: actionTypes.WALLPAPER_REMOVE_UPLOAD 14133 })); 14134 } 14135 14136 // Reset active wallpaper 14137 this.props.setPref("newtabWallpapers.wallpaper", ""); 14138 14139 // Fire WALLPAPER_CLICK telemetry event 14140 this.handleUserEvent(actionTypes.WALLPAPER_CLICK, { 14141 selected_wallpaper: "none", 14142 had_previous_wallpaper: !!this.props.activeWallpaper, 14143 had_uploaded_previously: !!uploadedPreviously 14144 }); 14145 } 14146 handleCategory = event => { 14147 this.setState({ 14148 activeCategory: event.target.id 14149 }); 14150 this.handleUserEvent(actionTypes.WALLPAPER_CATEGORY_CLICK, event.target.id); 14151 14152 // Notify parent menu when subpanel opens 14153 if (this.props.onSubpanelToggle) { 14154 this.props.onSubpanelToggle(true); 14155 } 14156 let fluent_id; 14157 switch (event.target.id) { 14158 case "abstracts": 14159 fluent_id = "newtab-wallpaper-category-title-abstract"; 14160 break; 14161 case "celestial": 14162 fluent_id = "newtab-wallpaper-category-title-celestial"; 14163 break; 14164 case "photographs": 14165 fluent_id = "newtab-wallpaper-category-title-photographs"; 14166 break; 14167 case "solid-colors": 14168 fluent_id = "newtab-wallpaper-category-title-colors"; 14169 break; 14170 case "firefox": 14171 fluent_id = "newtab-wallpaper-category-title-firefox"; 14172 break; 14173 } 14174 this.setState({ 14175 activeCategoryFluentID: fluent_id 14176 }); 14177 }; 14178 14179 // Custom wallpaper image upload 14180 async handleUpload() { 14181 const wallpaperUploadMaxFileSizeEnabled = this.props.Prefs.values[PREF_WALLPAPER_UPLOAD_MAX_FILE_SIZE_ENABLED]; 14182 const wallpaperUploadMaxFileSize = this.props.Prefs.values[PREF_WALLPAPER_UPLOAD_MAX_FILE_SIZE]; 14183 const uploadedPreviously = this.props.Prefs.values[PREF_WALLPAPER_UPLOADED_PREVIOUSLY]; 14184 14185 // Create a file input since category buttons are radio inputs 14186 const fileInput = document.createElement("input"); 14187 fileInput.type = "file"; 14188 fileInput.accept = "image/*"; // only allow image files 14189 14190 // Catch cancel events 14191 fileInput.oncancel = async () => { 14192 this.setState({ 14193 customWallpaperErrorType: null 14194 }); 14195 }; 14196 14197 // Reset error state when user begins file selection 14198 this.setState({ 14199 customWallpaperErrorType: null 14200 }); 14201 14202 // Fire when user selects a file 14203 fileInput.onchange = async event => { 14204 const [file] = event.target.files; 14205 if (file) { 14206 // Validate file type: Only accept files with a valid image MIME type 14207 const isValidImage = file.type && file.type.startsWith("image/"); 14208 if (!isValidImage) { 14209 console.error("Invalid file type"); 14210 this.setState({ 14211 customWallpaperErrorType: "fileType" 14212 }); 14213 return; 14214 } 14215 14216 // Limit image uploaded to a maximum file size if enabled 14217 // Note: The max file size pref (customWallpaper.fileSize) is converted to megabytes (MB) 14218 // Example: if pref value is 5, max file size is 5 MB 14219 const maxSize = wallpaperUploadMaxFileSize * 1024 * 1024; 14220 if (wallpaperUploadMaxFileSizeEnabled && file.size > maxSize) { 14221 console.error("File size exceeds limit"); 14222 this.setState({ 14223 customWallpaperErrorType: "fileSize" 14224 }); 14225 return; 14226 } 14227 this.props.dispatch(actionCreators.OnlyToMain({ 14228 type: actionTypes.WALLPAPER_UPLOAD, 14229 data: { 14230 file 14231 } 14232 })); 14233 14234 // Set active wallpaper ID to "custom" 14235 this.props.setPref("newtabWallpapers.wallpaper", "custom"); 14236 14237 // Update the uploadedPreviously pref to TRUE 14238 // Note: this pref used for telemetry. Do not reset to false. 14239 this.props.setPref(PREF_WALLPAPER_UPLOADED_PREVIOUSLY, true); 14240 this.handleUserEvent(actionTypes.WALLPAPER_CLICK, { 14241 selected_wallpaper: "custom", 14242 had_previous_wallpaper: !!this.props.activeWallpaper, 14243 had_uploaded_previously: !!uploadedPreviously 14244 }); 14245 } 14246 }; 14247 fileInput.click(); 14248 } 14249 handleBack() { 14250 this.setState({ 14251 activeCategory: null 14252 }, () => { 14253 // Notify parent menu when subpanel closes 14254 if (this.props.onSubpanelToggle) { 14255 this.props.onSubpanelToggle(false); 14256 } 14257 14258 // Wait for the category grid to be back in the DOM 14259 requestAnimationFrame(() => { 14260 this.focusCategory(this.state.focusedCategoryIndex); 14261 }); 14262 }); 14263 } 14264 handleWallpaperListEntered() { 14265 this.arrowButtonRef.current?.focus(); 14266 } 14267 14268 // Record user interaction when changing wallpaper and reseting wallpaper to default 14269 handleUserEvent(type, data) { 14270 this.props.dispatch(actionCreators.OnlyToMain({ 14271 type, 14272 data 14273 })); 14274 } 14275 setActiveId = id => { 14276 this.setState({ 14277 activeId: id 14278 }); // Set the active ID 14279 }; 14280 getRGBColors(input) { 14281 if (input.length !== 7) { 14282 return []; 14283 } 14284 const r = parseInt(input.substr(1, 2), 16); 14285 const g = parseInt(input.substr(3, 2), 16); 14286 const b = parseInt(input.substr(5, 2), 16); 14287 return [r, g, b]; 14288 } 14289 isWallpaperColorDark([r, g, b]) { 14290 return 0.2125 * r + 0.7154 * g + 0.0721 * b <= 110; 14291 } 14292 sortWallpapersByOrder(wallpapers) { 14293 return wallpapers.sort((a, b) => { 14294 const aOrder = a.order || 0; 14295 const bOrder = b.order || 0; 14296 if (aOrder === 0 && bOrder === 0) { 14297 return 0; 14298 } 14299 if (aOrder === 0) { 14300 return 1; 14301 } 14302 if (bOrder === 0) { 14303 return -1; 14304 } 14305 return aOrder - bOrder; 14306 }); 14307 } 14308 render() { 14309 const prefs = this.props.Prefs.values; 14310 const { 14311 wallpaperList, 14312 categories 14313 } = this.props.Wallpapers; 14314 const { 14315 activeWallpaper 14316 } = this.props; 14317 const { 14318 activeCategory, 14319 showColorPicker 14320 } = this.state; 14321 const { 14322 activeCategoryFluentID 14323 } = this.state; 14324 let filteredWallpapers = wallpaperList.filter(wallpaper => wallpaper.category === activeCategory); 14325 const wallpaperUploadMaxFileSize = this.props.Prefs.values[PREF_WALLPAPER_UPLOAD_MAX_FILE_SIZE]; 14326 function reduceColorsToFitCustomColorInput(arr) { 14327 // Reduce the amount of custom colors to make space for the custom color picker 14328 while (arr.length % 3 !== 2) { 14329 arr.pop(); 14330 } 14331 return arr; 14332 } 14333 let wallpaperCustomSolidColorHex = null; 14334 const selectedWallpaper = prefs["newtabWallpapers.wallpaper"]; 14335 14336 // User has previous selected a custom color 14337 if (selectedWallpaper.includes("solid-color-picker")) { 14338 this.setState({ 14339 showColorPicker: true 14340 }); 14341 const regex = /#([a-fA-F0-9]{6})/; 14342 [wallpaperCustomSolidColorHex] = selectedWallpaper.match(regex); 14343 } 14344 14345 // Enable custom color select if pref'ed on 14346 this.setState({ 14347 showColorPicker: prefs["newtabWallpapers.customColor.enabled"] 14348 }); 14349 14350 // Remove last item of solid colors to make space for custom color picker 14351 if (prefs["newtabWallpapers.customColor.enabled"] && activeCategory === "solid-colors") { 14352 filteredWallpapers = reduceColorsToFitCustomColorInput(filteredWallpapers); 14353 } 14354 14355 // Bug 1953012 - If nothing selected, default to color of customize panel 14356 // --color-blue-70 : #054096 14357 // --color-blue-05 : #deeafc 14358 const starterColorHex = this.prefersDarkQuery?.matches ? "#054096" : "#deeafc"; 14359 14360 // Set initial state of the color picker (depending if the user has already set a custom color) 14361 let initStateClassname = wallpaperCustomSolidColorHex ? "custom-color-set" : "default-color-set"; 14362 14363 // If a custom color picker is set, make sure the icon has the correct contrast 14364 if (wallpaperCustomSolidColorHex) { 14365 const rgbColors = this.getRGBColors(wallpaperCustomSolidColorHex); 14366 const isColorDark = this.isWallpaperColorDark(rgbColors); 14367 if (isColorDark) { 14368 initStateClassname += " custom-color-dark"; 14369 } 14370 } 14371 let colorPickerInput = showColorPicker && activeCategory === "solid-colors" ? /*#__PURE__*/external_React_default().createElement("div", { 14372 className: `theme-custom-color-picker ${initStateClassname}`, 14373 ref: this.customColorPickerRef 14374 }, /*#__PURE__*/external_React_default().createElement("input", { 14375 onInput: this.handleColorInput, 14376 onChange: this.debouncedHandleChange, 14377 onClick: () => this.setActiveId("solid-color-picker") // 14378 , 14379 type: "color", 14380 name: `wallpaper-solid-color-picker`, 14381 id: "solid-color-picker" 14382 // aria-checked is not applicable for input[type="color"] elements 14383 , 14384 "aria-current": this.state.activeId === "solid-color-picker", 14385 value: wallpaperCustomSolidColorHex || starterColorHex, 14386 className: `wallpaper-input 14387 ${this.state.activeId === "solid-color-picker" ? "active" : ""}`, 14388 ref: this.customColorInput 14389 }), /*#__PURE__*/external_React_default().createElement("label", { 14390 htmlFor: "solid-color-picker", 14391 "data-l10n-id": "newtab-wallpaper-custom-color" 14392 })) : ""; 14393 return /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement("div", { 14394 className: "category-header" 14395 }, /*#__PURE__*/external_React_default().createElement("h2", { 14396 "data-l10n-id": "newtab-wallpaper-title" 14397 }), /*#__PURE__*/external_React_default().createElement("button", { 14398 className: "wallpapers-reset", 14399 onClick: this.handleReset, 14400 "data-l10n-id": "newtab-wallpaper-reset" 14401 })), /*#__PURE__*/external_React_default().createElement("div", { 14402 role: "grid", 14403 "aria-label": "Wallpaper category selection. Use arrow keys to navigate." 14404 }, /*#__PURE__*/external_React_default().createElement("fieldset", { 14405 className: "category-list" 14406 }, categories.map((category, index) => { 14407 const filteredList = wallpaperList.filter(wallpaper => wallpaper.category === category); 14408 const sortedList = this.sortWallpapersByOrder(filteredList); 14409 const activeWallpaperObj = activeWallpaper && sortedList.find(wp => wp.title === activeWallpaper); 14410 // Detect custom solid color 14411 const isCustomSolidColor = category === "solid-colors" && activeWallpaper.startsWith("solid-color-picker"); 14412 const thumbnail = activeWallpaperObj || sortedList[0]; 14413 let fluent_id; 14414 switch (category) { 14415 case "abstracts": 14416 fluent_id = "newtab-wallpaper-category-title-abstract"; 14417 break; 14418 case "celestial": 14419 fluent_id = "newtab-wallpaper-category-title-celestial"; 14420 break; 14421 case "custom-wallpaper": 14422 fluent_id = "newtab-wallpaper-upload-image"; 14423 break; 14424 case "photographs": 14425 fluent_id = "newtab-wallpaper-category-title-photographs"; 14426 break; 14427 case "solid-colors": 14428 fluent_id = "newtab-wallpaper-category-title-colors"; 14429 break; 14430 case "firefox": 14431 fluent_id = "newtab-wallpaper-category-title-firefox"; 14432 break; 14433 } 14434 let style = {}; 14435 if (thumbnail?.wallpaperUrl) { 14436 style.backgroundImage = `url(${thumbnail.wallpaperUrl})`; 14437 style.backgroundPosition = thumbnail.background_position || "center"; 14438 } else { 14439 style.backgroundColor = thumbnail?.solid_color || ""; 14440 } 14441 // If custom solid color is active, override the thumbnail to the chosen hex 14442 if (isCustomSolidColor) { 14443 const hex = activeWallpaper.split("solid-color-picker-")[1] || ""; 14444 style.backgroundColor = hex; 14445 } 14446 const isCategorySelected = activeWallpaperObj || isCustomSolidColor; 14447 return /*#__PURE__*/external_React_default().createElement("div", { 14448 key: category 14449 }, /*#__PURE__*/external_React_default().createElement("button", WallpaperCategories_extends({ 14450 ref: el => { 14451 if (el) { 14452 this.categoryRef[index] = el; 14453 } 14454 }, 14455 id: category, 14456 style: style, 14457 onKeyDown: e => this.handleCategoryKeyDown(e, category) 14458 // Add overrides for custom wallpaper upload UI 14459 , 14460 onClick: event => { 14461 this.setState({ 14462 focusedCategoryIndex: index 14463 }); 14464 if (category !== "custom-wallpaper") { 14465 this.handleCategory(event); 14466 } else { 14467 this.handleUpload(); 14468 } 14469 }, 14470 className: `wallpaper-input 14471 ${category === "custom-wallpaper" ? "theme-custom-wallpaper" : ""} 14472 ${isCategorySelected ? "selected" : ""}`, 14473 tabIndex: this.state.focusedCategoryIndex === index ? 0 : -1 14474 }, category === "custom-wallpaper" ? { 14475 "aria-errormessage": "customWallpaperError" 14476 } : {})), /*#__PURE__*/external_React_default().createElement("label", { 14477 htmlFor: category, 14478 "data-l10n-id": fluent_id 14479 }, fluent_id)); 14480 })), this.state.customWallpaperErrorType && /*#__PURE__*/external_React_default().createElement("div", { 14481 className: "custom-wallpaper-error", 14482 id: "customWallpaperError" 14483 }, /*#__PURE__*/external_React_default().createElement("span", { 14484 className: "icon icon-info" 14485 }), (() => { 14486 switch (this.state.customWallpaperErrorType) { 14487 case "fileSize": 14488 return /*#__PURE__*/external_React_default().createElement("span", { 14489 "data-l10n-id": "newtab-wallpaper-error-max-file-size", 14490 "data-l10n-args": `{"file_size": ${wallpaperUploadMaxFileSize}}` 14491 }); 14492 case "fileType": 14493 return /*#__PURE__*/external_React_default().createElement("span", { 14494 "data-l10n-id": "newtab-wallpaper-error-upload-file-type" 14495 }); 14496 default: 14497 return null; 14498 } 14499 })())), /*#__PURE__*/external_React_default().createElement(external_ReactTransitionGroup_namespaceObject.CSSTransition, { 14500 in: !!activeCategory, 14501 timeout: 300, 14502 classNames: "wallpaper-list", 14503 unmountOnExit: true, 14504 onEntered: this.handleWallpaperListEntered 14505 }, /*#__PURE__*/external_React_default().createElement("section", { 14506 className: "category wallpaper-list ignore-color-mode" 14507 }, /*#__PURE__*/external_React_default().createElement("button", { 14508 ref: this.arrowButtonRef, 14509 className: "arrow-button", 14510 "data-l10n-id": activeCategoryFluentID, 14511 onClick: this.handleBack 14512 }), /*#__PURE__*/external_React_default().createElement("div", { 14513 role: "grid", 14514 "aria-label": "Wallpaper selection. Use arrow keys to navigate." 14515 }, /*#__PURE__*/external_React_default().createElement("fieldset", null, this.sortWallpapersByOrder(filteredWallpapers).map(({ 14516 background_position, 14517 fluent_id, 14518 solid_color, 14519 theme, 14520 title, 14521 wallpaperUrl 14522 }, index) => { 14523 let style = {}; 14524 if (wallpaperUrl) { 14525 style.backgroundImage = `url(${wallpaperUrl})`; 14526 style.backgroundPosition = background_position || "center"; 14527 } else { 14528 style.backgroundColor = solid_color || ""; 14529 } 14530 return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("input", { 14531 ref: el => { 14532 if (el) { 14533 this.wallpaperRef[index] = el; 14534 } 14535 }, 14536 onChange: this.handleChange, 14537 onKeyDown: e => this.handleWallpaperKeyDown(e, title), 14538 style: style, 14539 type: "radio", 14540 name: `wallpaper-${title}`, 14541 id: title, 14542 value: title, 14543 checked: title === activeWallpaper, 14544 "aria-checked": title === activeWallpaper, 14545 className: `wallpaper-input theme-${theme} ${this.state.activeId === title ? "active" : ""}`, 14546 onClick: () => this.setActiveId(title) // 14547 , 14548 tabIndex: index === 0 ? 0 : -1 //the first wallpaper in the array will have a tabindex of 0 so we can tab into it. The rest will have a tabindex of -1 14549 }), /*#__PURE__*/external_React_default().createElement("label", { 14550 htmlFor: title, 14551 className: "sr-only", 14552 "data-l10n-id": fluent_id 14553 }, fluent_id)); 14554 }), colorPickerInput))))); 14555 } 14556 } 14557 const WallpaperCategories = (0,external_ReactRedux_namespaceObject.connect)(state => { 14558 return { 14559 Wallpapers: state.Wallpapers, 14560 Prefs: state.Prefs 14561 }; 14562 })(_WallpaperCategories); 14563 ;// CONCATENATED MODULE: ./content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx 14564 function ContentSection_extends() { return ContentSection_extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, ContentSection_extends.apply(null, arguments); } 14565 /* This Source Code Form is subject to the terms of the Mozilla Public 14566 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 14567 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 14568 14569 14570 14571 14572 14573 class ContentSection extends (external_React_default()).PureComponent { 14574 constructor(props) { 14575 super(props); 14576 this.onPreferenceSelect = this.onPreferenceSelect.bind(this); 14577 14578 // Refs are necessary for dynamically measuring drawer heights for slide animations 14579 this.topSitesDrawerRef = /*#__PURE__*/external_React_default().createRef(); 14580 this.pocketDrawerRef = /*#__PURE__*/external_React_default().createRef(); 14581 } 14582 inputUserEvent(eventSource, eventValue) { 14583 this.props.dispatch(actionCreators.UserEvent({ 14584 event: "PREF_CHANGED", 14585 source: eventSource, 14586 value: { 14587 status: eventValue, 14588 menu_source: "CUSTOMIZE_MENU" 14589 } 14590 })); 14591 } 14592 onPreferenceSelect(e) { 14593 // eventSource: WEATHER | TOP_SITES | TOP_STORIES | WIDGET_LISTS | WIDGET_TIMER 14594 const { 14595 preference, 14596 eventSource 14597 } = e.target.dataset; 14598 let value; 14599 if (e.target.nodeName === "SELECT") { 14600 value = parseInt(e.target.value, 10); 14601 } else if (e.target.nodeName === "INPUT") { 14602 value = e.target.checked; 14603 if (eventSource) { 14604 this.inputUserEvent(eventSource, value); 14605 } 14606 } else if (e.target.nodeName === "MOZ-TOGGLE") { 14607 value = e.target.pressed; 14608 if (eventSource) { 14609 this.inputUserEvent(eventSource, value); 14610 } 14611 } 14612 this.props.setPref(preference, value); 14613 } 14614 componentDidMount() { 14615 this.setDrawerMargins(); 14616 } 14617 componentDidUpdate() { 14618 this.setDrawerMargins(); 14619 } 14620 setDrawerMargins() { 14621 this.setDrawerMargin(`TOP_SITES`, this.props.enabledSections.topSitesEnabled); 14622 this.setDrawerMargin(`TOP_STORIES`, this.props.enabledSections.pocketEnabled); 14623 } 14624 setDrawerMargin(drawerID, isOpen) { 14625 let drawerRef; 14626 if (drawerID === `TOP_SITES`) { 14627 drawerRef = this.topSitesDrawerRef.current; 14628 } else if (drawerID === `TOP_STORIES`) { 14629 drawerRef = this.pocketDrawerRef.current; 14630 } else { 14631 return; 14632 } 14633 if (drawerRef) { 14634 // Use measured height if valid, otherwise use a large fallback 14635 // since overflow:hidden on the parent safely hides the drawer 14636 let drawerHeight = parseFloat(window.getComputedStyle(drawerRef)?.height) || 100; 14637 if (isOpen) { 14638 drawerRef.style.marginTop = "var(--space-small)"; 14639 } else { 14640 drawerRef.style.marginTop = `-${drawerHeight + 3}px`; 14641 } 14642 } 14643 } 14644 render() { 14645 const { 14646 enabledSections, 14647 enabledWidgets, 14648 pocketRegion, 14649 mayHaveInferredPersonalization, 14650 mayHaveWeather, 14651 mayHaveWidgets, 14652 mayHaveTimerWidget, 14653 mayHaveListsWidget, 14654 openPreferences, 14655 wallpapersEnabled, 14656 activeWallpaper, 14657 setPref, 14658 mayHaveTopicSections, 14659 exitEventFired, 14660 onSubpanelToggle, 14661 toggleSectionsMgmtPanel, 14662 showSectionsMgmtPanel 14663 } = this.props; 14664 const { 14665 topSitesEnabled, 14666 pocketEnabled, 14667 weatherEnabled, 14668 showInferredPersonalizationEnabled, 14669 topSitesRowsCount 14670 } = enabledSections; 14671 const { 14672 timerEnabled, 14673 listsEnabled 14674 } = enabledWidgets; 14675 return /*#__PURE__*/external_React_default().createElement("div", { 14676 className: "home-section" 14677 }, wallpapersEnabled && /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("div", { 14678 className: "wallpapers-section" 14679 }, /*#__PURE__*/external_React_default().createElement(WallpaperCategories, { 14680 setPref: setPref, 14681 activeWallpaper: activeWallpaper, 14682 exitEventFired: exitEventFired, 14683 onSubpanelToggle: onSubpanelToggle 14684 })), !mayHaveWidgets && /*#__PURE__*/external_React_default().createElement("span", { 14685 className: "divider", 14686 role: "separator" 14687 })), mayHaveWidgets && /*#__PURE__*/external_React_default().createElement("div", { 14688 className: "widgets-section" 14689 }, /*#__PURE__*/external_React_default().createElement("div", { 14690 className: "category-header" 14691 }, /*#__PURE__*/external_React_default().createElement("h2", { 14692 "data-l10n-id": "newtab-custom-widget-section-title" 14693 })), /*#__PURE__*/external_React_default().createElement("div", { 14694 className: "settings-widgets" 14695 }, mayHaveWeather && /*#__PURE__*/external_React_default().createElement("div", { 14696 id: "weather-section", 14697 className: "section" 14698 }, /*#__PURE__*/external_React_default().createElement("moz-toggle", { 14699 id: "weather-toggle", 14700 pressed: weatherEnabled || null, 14701 onToggle: this.onPreferenceSelect, 14702 "data-preference": "showWeather", 14703 "data-eventSource": "WEATHER", 14704 "data-l10n-id": "newtab-custom-widget-weather-toggle" 14705 })), mayHaveListsWidget && /*#__PURE__*/external_React_default().createElement("div", { 14706 id: "lists-widget-section", 14707 className: "section" 14708 }, /*#__PURE__*/external_React_default().createElement("moz-toggle", { 14709 id: "lists-toggle", 14710 pressed: listsEnabled || null, 14711 onToggle: this.onPreferenceSelect, 14712 "data-preference": "widgets.lists.enabled", 14713 "data-eventSource": "WIDGET_LISTS", 14714 "data-l10n-id": "newtab-custom-widget-lists-toggle" 14715 })), mayHaveTimerWidget && /*#__PURE__*/external_React_default().createElement("div", { 14716 id: "timer-widget-section", 14717 className: "section" 14718 }, /*#__PURE__*/external_React_default().createElement("moz-toggle", { 14719 id: "timer-toggle", 14720 pressed: timerEnabled || null, 14721 onToggle: this.onPreferenceSelect, 14722 "data-preference": "widgets.focusTimer.enabled", 14723 "data-eventSource": "WIDGET_TIMER", 14724 "data-l10n-id": "newtab-custom-widget-timer-toggle" 14725 })), /*#__PURE__*/external_React_default().createElement("span", { 14726 className: "divider", 14727 role: "separator" 14728 }))), /*#__PURE__*/external_React_default().createElement("div", { 14729 className: "settings-toggles" 14730 }, !mayHaveWidgets && mayHaveWeather && /*#__PURE__*/external_React_default().createElement("div", { 14731 id: "weather-section", 14732 className: "section" 14733 }, /*#__PURE__*/external_React_default().createElement("moz-toggle", { 14734 id: "weather-toggle", 14735 pressed: weatherEnabled || null, 14736 onToggle: this.onPreferenceSelect, 14737 "data-preference": "showWeather", 14738 "data-eventSource": "WEATHER", 14739 "data-l10n-id": "newtab-custom-weather-toggle" 14740 })), /*#__PURE__*/external_React_default().createElement("div", { 14741 id: "shortcuts-section", 14742 className: "section" 14743 }, /*#__PURE__*/external_React_default().createElement("moz-toggle", { 14744 id: "shortcuts-toggle", 14745 pressed: topSitesEnabled || null, 14746 onToggle: this.onPreferenceSelect, 14747 "data-preference": "feeds.topsites", 14748 "data-eventSource": "TOP_SITES", 14749 "data-l10n-id": "newtab-custom-shortcuts-toggle" 14750 }, /*#__PURE__*/external_React_default().createElement("div", { 14751 slot: "nested" 14752 }, /*#__PURE__*/external_React_default().createElement("div", { 14753 className: "more-info-top-wrapper" 14754 }, /*#__PURE__*/external_React_default().createElement("div", { 14755 className: "more-information", 14756 ref: this.topSitesDrawerRef 14757 }, /*#__PURE__*/external_React_default().createElement("select", { 14758 id: "row-selector", 14759 className: "selector", 14760 name: "row-count", 14761 "data-preference": "topSitesRows", 14762 value: topSitesRowsCount, 14763 onChange: this.onPreferenceSelect, 14764 disabled: !topSitesEnabled, 14765 "aria-labelledby": "custom-shortcuts-title" 14766 }, /*#__PURE__*/external_React_default().createElement("option", { 14767 value: "1", 14768 "data-l10n-id": "newtab-custom-row-selector", 14769 "data-l10n-args": "{\"num\": 1}" 14770 }), /*#__PURE__*/external_React_default().createElement("option", { 14771 value: "2", 14772 "data-l10n-id": "newtab-custom-row-selector", 14773 "data-l10n-args": "{\"num\": 2}" 14774 }), /*#__PURE__*/external_React_default().createElement("option", { 14775 value: "3", 14776 "data-l10n-id": "newtab-custom-row-selector", 14777 "data-l10n-args": "{\"num\": 3}" 14778 }), /*#__PURE__*/external_React_default().createElement("option", { 14779 value: "4", 14780 "data-l10n-id": "newtab-custom-row-selector", 14781 "data-l10n-args": "{\"num\": 4}" 14782 }))))))), pocketRegion && /*#__PURE__*/external_React_default().createElement("div", { 14783 id: "pocket-section", 14784 className: "section" 14785 }, /*#__PURE__*/external_React_default().createElement("moz-toggle", ContentSection_extends({ 14786 id: "pocket-toggle", 14787 pressed: pocketEnabled || null, 14788 onToggle: this.onPreferenceSelect, 14789 "aria-describedby": "custom-pocket-subtitle", 14790 "data-preference": "feeds.section.topstories", 14791 "data-eventSource": "TOP_STORIES" 14792 }, mayHaveInferredPersonalization ? { 14793 "data-l10n-id": "newtab-custom-stories-personalized-toggle" 14794 } : { 14795 "data-l10n-id": "newtab-custom-stories-toggle" 14796 }), /*#__PURE__*/external_React_default().createElement("div", { 14797 slot: "nested" 14798 }, (mayHaveInferredPersonalization || mayHaveTopicSections) && /*#__PURE__*/external_React_default().createElement("div", { 14799 className: "more-info-pocket-wrapper" 14800 }, /*#__PURE__*/external_React_default().createElement("div", { 14801 className: "more-information", 14802 ref: this.pocketDrawerRef 14803 }, mayHaveInferredPersonalization && /*#__PURE__*/external_React_default().createElement("div", { 14804 className: "check-wrapper", 14805 role: "presentation" 14806 }, /*#__PURE__*/external_React_default().createElement("input", { 14807 id: "inferred-personalization", 14808 className: "customize-menu-checkbox", 14809 disabled: !pocketEnabled, 14810 checked: showInferredPersonalizationEnabled, 14811 type: "checkbox", 14812 onChange: this.onPreferenceSelect, 14813 "data-preference": "discoverystream.sections.personalization.inferred.user.enabled", 14814 "data-eventSource": "INFERRED_PERSONALIZATION" 14815 }), /*#__PURE__*/external_React_default().createElement("label", { 14816 className: "customize-menu-checkbox-label", 14817 htmlFor: "inferred-personalization", 14818 "data-l10n-id": "newtab-custom-stories-personalized-checkbox-label" 14819 })), mayHaveTopicSections && /*#__PURE__*/external_React_default().createElement(SectionsMgmtPanel, { 14820 exitEventFired: exitEventFired, 14821 pocketEnabled: pocketEnabled, 14822 onSubpanelToggle: onSubpanelToggle, 14823 togglePanel: toggleSectionsMgmtPanel, 14824 showPanel: showSectionsMgmtPanel 14825 }))))))), /*#__PURE__*/external_React_default().createElement("span", { 14826 className: "divider", 14827 role: "separator" 14828 }), /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement("button", { 14829 id: "settings-link", 14830 className: "external-link", 14831 onClick: openPreferences, 14832 "data-l10n-id": "newtab-custom-settings" 14833 }))); 14834 } 14835 } 14836 ;// CONCATENATED MODULE: ./content-src/components/CustomizeMenu/CustomizeMenu.jsx 14837 /* This Source Code Form is subject to the terms of the Mozilla Public 14838 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 14839 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 14840 14841 14842 14843 14844 // eslint-disable-next-line no-shadow 14845 14846 class _CustomizeMenu extends (external_React_default()).PureComponent { 14847 constructor(props) { 14848 super(props); 14849 this.onEntered = this.onEntered.bind(this); 14850 this.onExited = this.onExited.bind(this); 14851 this.onSubpanelToggle = this.onSubpanelToggle.bind(this); 14852 this.state = { 14853 exitEventFired: false, 14854 subpanelOpen: false 14855 }; 14856 } 14857 onSubpanelToggle(isOpen) { 14858 this.setState({ 14859 subpanelOpen: isOpen 14860 }); 14861 } 14862 onEntered() { 14863 this.setState({ 14864 exitEventFired: false 14865 }); 14866 if (this.closeButton) { 14867 this.closeButton.focus(); 14868 } 14869 } 14870 onExited() { 14871 this.setState({ 14872 exitEventFired: true 14873 }); 14874 if (this.openButton) { 14875 this.openButton.focus(); 14876 } 14877 } 14878 render() { 14879 return /*#__PURE__*/external_React_default().createElement("span", null, /*#__PURE__*/external_React_default().createElement(external_ReactTransitionGroup_namespaceObject.CSSTransition, { 14880 timeout: 300, 14881 classNames: "personalize-animate", 14882 in: !this.props.showing, 14883 appear: true 14884 }, /*#__PURE__*/external_React_default().createElement("button", { 14885 className: "personalize-button", 14886 "data-l10n-id": "newtab-customize-panel-icon-button", 14887 onClick: () => this.props.onOpen(), 14888 onKeyDown: e => { 14889 if (e.key === "Enter") { 14890 this.props.onOpen(); 14891 } 14892 }, 14893 ref: c => this.openButton = c 14894 }, /*#__PURE__*/external_React_default().createElement("label", { 14895 "data-l10n-id": "newtab-customize-panel-icon-button-label" 14896 }), /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement("img", { 14897 role: "presentation", 14898 src: "chrome://global/skin/icons/edit-outline.svg" 14899 })))), /*#__PURE__*/external_React_default().createElement(external_ReactTransitionGroup_namespaceObject.CSSTransition, { 14900 timeout: 250, 14901 classNames: "customize-animate", 14902 in: this.props.showing, 14903 onEntered: this.onEntered, 14904 onExited: this.onExited, 14905 appear: true 14906 }, /*#__PURE__*/external_React_default().createElement("div", { 14907 className: "customize-menu-animate-wrapper" 14908 }, /*#__PURE__*/external_React_default().createElement("div", { 14909 className: `customize-menu ${this.state.subpanelOpen ? "subpanel-open" : ""}`, 14910 role: "dialog", 14911 "data-l10n-id": "newtab-settings-dialog-label" 14912 }, /*#__PURE__*/external_React_default().createElement("div", { 14913 className: "close-button-wrapper" 14914 }, /*#__PURE__*/external_React_default().createElement("moz-button", { 14915 onClick: () => this.props.onClose(), 14916 id: "close-button", 14917 type: "icon ghost", 14918 "data-l10n-id": "newtab-custom-close-menu-button", 14919 iconsrc: "chrome://global/skin/icons/close.svg", 14920 ref: c => this.closeButton = c 14921 })), /*#__PURE__*/external_React_default().createElement(ContentSection, { 14922 openPreferences: this.props.openPreferences, 14923 setPref: this.props.setPref, 14924 enabledSections: this.props.enabledSections, 14925 enabledWidgets: this.props.enabledWidgets, 14926 wallpapersEnabled: this.props.wallpapersEnabled, 14927 activeWallpaper: this.props.activeWallpaper, 14928 pocketRegion: this.props.pocketRegion, 14929 mayHaveTopicSections: this.props.mayHaveTopicSections, 14930 mayHaveInferredPersonalization: this.props.mayHaveInferredPersonalization, 14931 mayHaveWeather: this.props.mayHaveWeather, 14932 mayHaveWidgets: this.props.mayHaveWidgets, 14933 mayHaveTimerWidget: this.props.mayHaveTimerWidget, 14934 mayHaveListsWidget: this.props.mayHaveListsWidget, 14935 dispatch: this.props.dispatch, 14936 exitEventFired: this.state.exitEventFired, 14937 onSubpanelToggle: this.onSubpanelToggle, 14938 toggleSectionsMgmtPanel: this.props.toggleSectionsMgmtPanel, 14939 showSectionsMgmtPanel: this.props.showSectionsMgmtPanel 14940 }))))); 14941 } 14942 } 14943 const CustomizeMenu = (0,external_ReactRedux_namespaceObject.connect)(state => ({ 14944 DiscoveryStream: state.DiscoveryStream 14945 }))(_CustomizeMenu); 14946 ;// CONCATENATED MODULE: ./content-src/components/Logo/Logo.jsx 14947 /* This Source Code Form is subject to the terms of the Mozilla Public 14948 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 14949 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 14950 14951 14952 function Logo() { 14953 return /*#__PURE__*/external_React_default().createElement("h1", { 14954 className: "logo-and-wordmark-wrapper" 14955 }, /*#__PURE__*/external_React_default().createElement("div", { 14956 className: "logo-and-wordmark", 14957 role: "img", 14958 "data-l10n-id": "newtab-logo-and-wordmark" 14959 }, /*#__PURE__*/external_React_default().createElement("div", { 14960 className: "logo" 14961 }), /*#__PURE__*/external_React_default().createElement("div", { 14962 className: "wordmark" 14963 }))); 14964 } 14965 14966 ;// CONCATENATED MODULE: ./content-src/components/ExternalComponentWrapper/ExternalComponentWrapper.jsx 14967 /* This Source Code Form is subject to the terms of the Mozilla Public 14968 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 14969 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 14970 14971 14972 14973 14974 /** 14975 * A React component that dynamically loads and embeds external custom elements 14976 * into the newtab page. 14977 * 14978 * This component serves as a bridge between React's declarative rendering and 14979 * browser-native custom elements that are registered and managed outside of 14980 * React's control. It: 14981 * 14982 * 1. Looks up the component configuration by type from the ExternalComponents 14983 * registry 14984 * 2. Dynamically imports the component's script module (which registers the 14985 * custom element) 14986 * 3. Creates an instance of the custom element using imperative DOM APIs 14987 * 4. Appends it to a React-managed container div 14988 * 5. Cleans up the custom element on unmount 14989 * 14990 * This approach is necessary because: 14991 * - Custom elements have their own lifecycle separate from React 14992 * - They need to be created imperatively (document.createElement) rather than 14993 * declaratively (JSX) 14994 * - React shouldn't try to diff/reconcile their internal DOM, as they manage 14995 * their own shadow DOM 14996 * - We need manual cleanup to prevent memory leaks when the component unmounts 14997 * 14998 * @param {object} props 14999 * @param {string} props.type - The component type to load (e.g., "SEARCH") 15000 * @param {string} props.className - CSS class name(s) to apply to the wrapper div 15001 * @param {Function} props.importModule - Function to import modules (for testing) 15002 */ 15003 function ExternalComponentWrapper({ 15004 type, 15005 className, 15006 // importFunction is declared as an arrow function here purely so that we can 15007 // override it for testing. 15008 // eslint-disable-next-line no-unsanitized/method 15009 importModule = url => import(/* webpackIgnore: true */url) 15010 }) { 15011 const containerRef = external_React_default().useRef(null); 15012 const customElementRef = external_React_default().useRef(null); 15013 const l10nLinksRef = external_React_default().useRef([]); 15014 const [error, setError] = external_React_default().useState(null); 15015 const { 15016 components 15017 } = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.ExternalComponents); 15018 external_React_default().useEffect(() => { 15019 const container = containerRef.current; 15020 const loadComponent = async () => { 15021 try { 15022 const config = components.find(c => c.type === type); 15023 if (!config) { 15024 console.warn(`No external component configuration found for type: ${type}`); 15025 return; 15026 } 15027 await importModule(config.componentURL); 15028 l10nLinksRef.current = []; 15029 for (let l10nURL of config.l10nURLs) { 15030 const l10nEl = document.createElement("link"); 15031 l10nEl.rel = "localization"; 15032 l10nEl.href = l10nURL; 15033 document.head.appendChild(l10nEl); 15034 l10nLinksRef.current.push(l10nEl); 15035 } 15036 if (containerRef.current && !customElementRef.current) { 15037 const element = document.createElement(config.tagName); 15038 if (config.attributes) { 15039 for (const [key, value] of Object.entries(config.attributes)) { 15040 element.setAttribute(key, value); 15041 } 15042 } 15043 if (config.cssVariables) { 15044 for (const [variable, style] of Object.entries(config.cssVariables)) { 15045 element.style.setProperty(variable, style); 15046 } 15047 } 15048 customElementRef.current = element; 15049 containerRef.current.appendChild(element); 15050 } 15051 } catch (err) { 15052 console.error(`Failed to load external component for type ${type}:`, err); 15053 setError(err); 15054 } 15055 }; 15056 loadComponent(); 15057 return () => { 15058 if (customElementRef.current && container) { 15059 container.removeChild(customElementRef.current); 15060 customElementRef.current = null; 15061 } 15062 for (const link of l10nLinksRef.current) { 15063 link.remove(); 15064 } 15065 l10nLinksRef.current = []; 15066 }; 15067 }, [type, components, importModule]); 15068 if (error) { 15069 return null; 15070 } 15071 return /*#__PURE__*/external_React_default().createElement("div", { 15072 ref: containerRef, 15073 className: className 15074 }); 15075 } 15076 15077 ;// CONCATENATED MODULE: ./content-src/components/Search/Search.jsx 15078 /* This Source Code Form is subject to the terms of the Mozilla Public 15079 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 15080 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 15081 15082 /* globals ContentSearchHandoffUIController */ 15083 15084 /** 15085 * @backward-compat { version 148 } 15086 * 15087 * Temporary dual implementation to support train hopping. The old handoff UI 15088 * is kept alongside the new contentSearchHandoffUI.mjs custom element until 15089 * the module lands on all channels. Controlled by the pref 15090 * browser.newtabpage.activity-stream.search.useHandoffComponent. 15091 * Remove the old implementation and the pref once this ships to Release. 15092 */ 15093 15094 15095 15096 15097 15098 15099 class _Search extends (external_React_default()).PureComponent { 15100 constructor(props) { 15101 super(props); 15102 this.onSearchHandoffClick = this.onSearchHandoffClick.bind(this); 15103 this.onSearchHandoffPaste = this.onSearchHandoffPaste.bind(this); 15104 this.onSearchHandoffDrop = this.onSearchHandoffDrop.bind(this); 15105 this.onInputMountHandoff = this.onInputMountHandoff.bind(this); 15106 this.onSearchHandoffButtonMount = this.onSearchHandoffButtonMount.bind(this); 15107 } 15108 handleEvent(event) { 15109 // Also track search events with our own telemetry 15110 if (event.detail.type === "Search") { 15111 this.props.dispatch(actionCreators.UserEvent({ 15112 event: "SEARCH" 15113 })); 15114 } 15115 } 15116 doSearchHandoff(text) { 15117 this.props.dispatch(actionCreators.OnlyToMain({ 15118 type: actionTypes.HANDOFF_SEARCH_TO_AWESOMEBAR, 15119 data: { 15120 text 15121 } 15122 })); 15123 this.props.dispatch({ 15124 type: actionTypes.FAKE_FOCUS_SEARCH 15125 }); 15126 this.props.dispatch(actionCreators.UserEvent({ 15127 event: "SEARCH_HANDOFF" 15128 })); 15129 if (text) { 15130 this.props.dispatch({ 15131 type: actionTypes.DISABLE_SEARCH 15132 }); 15133 } 15134 } 15135 onSearchHandoffClick(event) { 15136 // When search hand-off is enabled, we render a big button that is styled to 15137 // look like a search textbox. If the button is clicked, we style 15138 // the button as if it was a focused search box and show a fake cursor but 15139 // really focus the awesomebar without the focus styles ("hidden focus"). 15140 event.preventDefault(); 15141 this.doSearchHandoff(); 15142 } 15143 onSearchHandoffPaste(event) { 15144 event.preventDefault(); 15145 this.doSearchHandoff(event.clipboardData.getData("Text")); 15146 } 15147 onSearchHandoffDrop(event) { 15148 event.preventDefault(); 15149 let text = event.dataTransfer.getData("text"); 15150 if (text) { 15151 this.doSearchHandoff(text); 15152 } 15153 } 15154 componentDidMount() { 15155 const { 15156 caretBlinkCount, 15157 caretBlinkTime, 15158 "search.useHandoffComponent": useHandoffComponent, 15159 "externalComponents.enabled": useExternalComponents 15160 } = this.props.Prefs.values; 15161 if (useExternalComponents) { 15162 // Nothing to do - the external component will have set the caret 15163 // values itself. 15164 return; 15165 } 15166 if (useHandoffComponent) { 15167 const { 15168 handoffUI 15169 } = this; 15170 if (handoffUI) { 15171 // If caret blink count isn't defined, use the default infinite behavior for animation 15172 handoffUI.style.setProperty("--caret-blink-count", caretBlinkCount > -1 ? caretBlinkCount : "infinite"); 15173 15174 // Apply custom blink rate if set, else fallback to default (567ms on/off --> 1134ms total) 15175 handoffUI.style.setProperty("--caret-blink-time", caretBlinkTime > 0 ? `${caretBlinkTime * 2}ms` : `${1134}ms`); 15176 } 15177 } else { 15178 const caret = this.fakeCaret; 15179 if (caret) { 15180 // If caret blink count isn't defined, use the default infinite behavior for animation 15181 caret.style.setProperty("--caret-blink-count", caretBlinkCount > -1 ? caretBlinkCount : "infinite"); 15182 15183 // Apply custom blink rate if set, else fallback to default (567ms on/off --> 1134ms total) 15184 caret.style.setProperty("--caret-blink-time", caretBlinkTime > 0 ? `${caretBlinkTime * 2}ms` : `${1134}ms`); 15185 } 15186 } 15187 } 15188 onInputMountHandoff(input) { 15189 if (input) { 15190 // The handoff UI controller helps us set the search icon and reacts to 15191 // changes to default engine to keep everything in sync. 15192 this._handoffSearchController = new ContentSearchHandoffUIController(); 15193 } 15194 } 15195 onSearchHandoffButtonMount(button) { 15196 // Keep a reference to the button for use during "paste" event handling. 15197 this._searchHandoffButton = button; 15198 } 15199 15200 /* 15201 * Do not change the ID on the input field, as legacy newtab code 15202 * specifically looks for the id 'newtab-search-text' on input fields 15203 * in order to execute searches in various tests 15204 */ 15205 render() { 15206 const useHandoffComponent = this.props.Prefs.values["search.useHandoffComponent"]; 15207 const useExternalComponents = this.props.Prefs.values["externalComponents.enabled"]; 15208 if (useHandoffComponent) { 15209 if (useExternalComponents) { 15210 return /*#__PURE__*/external_React_default().createElement("div", { 15211 className: "search-wrapper" 15212 }, this.props.showLogo && /*#__PURE__*/external_React_default().createElement(Logo, null), /*#__PURE__*/external_React_default().createElement(ExternalComponentWrapper, { 15213 type: "SEARCH", 15214 className: "search-inner-wrapper" 15215 })); 15216 } 15217 return /*#__PURE__*/external_React_default().createElement("div", { 15218 className: "search-wrapper" 15219 }, this.props.showLogo && /*#__PURE__*/external_React_default().createElement(Logo, null), /*#__PURE__*/external_React_default().createElement("div", { 15220 className: "search-inner-wrapper" 15221 }, /*#__PURE__*/external_React_default().createElement("content-search-handoff-ui", { 15222 ref: el => { 15223 this.handoffUI = el; 15224 } 15225 }))); 15226 } 15227 const wrapperClassName = ["search-wrapper", this.props.disable && "search-disabled", this.props.fakeFocus && "fake-focus"].filter(v => v).join(" "); 15228 return /*#__PURE__*/external_React_default().createElement("div", { 15229 className: wrapperClassName 15230 }, this.props.showLogo && /*#__PURE__*/external_React_default().createElement(Logo, null), /*#__PURE__*/external_React_default().createElement("div", { 15231 className: "search-inner-wrapper" 15232 }, /*#__PURE__*/external_React_default().createElement("button", { 15233 className: "search-handoff-button", 15234 ref: this.onSearchHandoffButtonMount, 15235 onClick: this.onSearchHandoffClick, 15236 tabIndex: "-1" 15237 }, /*#__PURE__*/external_React_default().createElement("div", { 15238 className: "fake-textbox" 15239 }), /*#__PURE__*/external_React_default().createElement("input", { 15240 type: "search", 15241 className: "fake-editable", 15242 tabIndex: "-1", 15243 "aria-hidden": "true", 15244 onDrop: this.onSearchHandoffDrop, 15245 onPaste: this.onSearchHandoffPaste, 15246 ref: this.onInputMountHandoff 15247 }), /*#__PURE__*/external_React_default().createElement("div", { 15248 className: "fake-caret", 15249 ref: el => { 15250 this.fakeCaret = el; 15251 } 15252 })))); 15253 } 15254 } 15255 const Search_Search = (0,external_ReactRedux_namespaceObject.connect)(state => ({ 15256 Prefs: state.Prefs 15257 }))(_Search); 15258 ;// CONCATENATED MODULE: ./content-src/components/DownloadModalToggle/DownloadModalToggle.jsx 15259 /* This Source Code Form is subject to the terms of the Mozilla Public 15260 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 15261 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 15262 15263 15264 function DownloadModalToggle({ 15265 onClick, 15266 isActive 15267 }) { 15268 return /*#__PURE__*/external_React_default().createElement("button", { 15269 className: `mobile-download-promo ${isActive ? " is-active" : ""}`, 15270 onClick: onClick 15271 }, /*#__PURE__*/external_React_default().createElement("div", { 15272 className: "icon icon-device-phone" 15273 })); 15274 } 15275 15276 ;// CONCATENATED MODULE: ./content-src/components/Notifications/Toasts/ReportContentToast.jsx 15277 /* This Source Code Form is subject to the terms of the Mozilla Public 15278 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 15279 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 15280 15281 15282 function ReportContentToast({ 15283 onDismissClick, 15284 onAnimationEnd 15285 }) { 15286 const mozMessageBarRef = (0,external_React_namespaceObject.useRef)(null); 15287 (0,external_React_namespaceObject.useEffect)(() => { 15288 const { 15289 current: mozMessageBarElement 15290 } = mozMessageBarRef; 15291 mozMessageBarElement.addEventListener("message-bar:user-dismissed", onDismissClick, { 15292 once: true 15293 }); 15294 return () => { 15295 mozMessageBarElement.removeEventListener("message-bar:user-dismissed", onDismissClick); 15296 }; 15297 }, [onDismissClick]); 15298 return /*#__PURE__*/external_React_default().createElement("moz-message-bar", { 15299 type: "success", 15300 class: "notification-feed-item", 15301 dismissable: true, 15302 "data-l10n-id": "newtab-toast-thanks-for-reporting", 15303 ref: mozMessageBarRef, 15304 onAnimationEnd: onAnimationEnd 15305 }); 15306 } 15307 15308 ;// CONCATENATED MODULE: ./content-src/components/Notifications/Notifications.jsx 15309 /* This Source Code Form is subject to the terms of the Mozilla Public 15310 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 15311 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 15312 15313 15314 15315 15316 15317 function Notifications_Notifications({ 15318 dispatch 15319 }) { 15320 const toastQueue = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Notifications.toastQueue); 15321 const toastCounter = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Notifications.toastCounter); 15322 15323 /** 15324 * Syncs {@link toastQueue} array so it can be used to 15325 * remove the toasts wrapper if there are none after a 15326 * toast is auto-hidden (animated out) via CSS. 15327 */ 15328 const syncHiddenToastData = (0,external_React_namespaceObject.useCallback)(() => { 15329 const toastId = toastQueue[toastQueue.length - 1]; 15330 const queuedToasts = [...toastQueue].slice(1); 15331 dispatch(actionCreators.OnlyToOneContent({ 15332 type: actionTypes.HIDE_TOAST_MESSAGE, 15333 data: { 15334 toastQueue: queuedToasts, 15335 toastCounter: queuedToasts.length, 15336 toastId, 15337 showNotifications: false 15338 } 15339 }, "ActivityStream:Content")); 15340 }, [dispatch, toastQueue]); 15341 const getToast = (0,external_React_namespaceObject.useCallback)(() => { 15342 // Note: This architecture could expand to support multiple toast notifications at once 15343 const latestToastItem = toastQueue[toastQueue.length - 1]; 15344 if (!latestToastItem) { 15345 throw new Error("No toast found"); 15346 } 15347 switch (latestToastItem) { 15348 case "reportSuccessToast": 15349 return /*#__PURE__*/external_React_default().createElement(ReportContentToast, { 15350 onDismissClick: syncHiddenToastData, 15351 onAnimationEnd: syncHiddenToastData, 15352 key: toastCounter 15353 }); 15354 default: 15355 throw new Error(`Unexpected toast type: ${latestToastItem}`); 15356 } 15357 }, [syncHiddenToastData, toastCounter, toastQueue]); 15358 (0,external_React_namespaceObject.useEffect)(() => { 15359 getToast(); 15360 }, [toastQueue, getToast]); 15361 return toastQueue.length ? /*#__PURE__*/external_React_default().createElement("div", { 15362 className: "notification-wrapper" 15363 }, getToast()) : ""; 15364 } 15365 15366 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/TopicSelection/TopicSelection.jsx 15367 /* This Source Code Form is subject to the terms of the Mozilla Public 15368 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 15369 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 15370 15371 15372 15373 15374 15375 const EMOJI_LABELS = { 15376 business: "💼", 15377 arts: "🎭", 15378 food: "🍕", 15379 health: "🩺", 15380 finance: "💰", 15381 government: "🏛️", 15382 sports: "⚽️", 15383 tech: "💻", 15384 travel: "✈️", 15385 "education-science": "🧪", 15386 society: "💡" 15387 }; 15388 function TopicSelection({ 15389 supportUrl 15390 }) { 15391 const dispatch = (0,external_ReactRedux_namespaceObject.useDispatch)(); 15392 const inputRef = (0,external_React_namespaceObject.useRef)(null); 15393 const modalRef = (0,external_React_namespaceObject.useRef)(null); 15394 const checkboxWrapperRef = (0,external_React_namespaceObject.useRef)(null); 15395 const prefs = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Prefs.values); 15396 const topics = prefs["discoverystream.topicSelection.topics"].split(", "); 15397 const selectedTopics = prefs["discoverystream.topicSelection.selectedTopics"]; 15398 const suggestedTopics = prefs["discoverystream.topicSelection.suggestedTopics"]?.split(", "); 15399 const displayCount = prefs["discoverystream.topicSelection.onboarding.displayCount"]; 15400 const topicsHaveBeenPreviouslySet = prefs["discoverystream.topicSelection.hasBeenUpdatedPreviously"]; 15401 const [isFirstRun] = (0,external_React_namespaceObject.useState)(displayCount === 0); 15402 const displayCountRef = (0,external_React_namespaceObject.useRef)(displayCount); 15403 const preselectedTopics = () => { 15404 if (selectedTopics) { 15405 return selectedTopics.split(", "); 15406 } 15407 return isFirstRun ? suggestedTopics : []; 15408 }; 15409 const [topicsToSelect, setTopicsToSelect] = (0,external_React_namespaceObject.useState)(preselectedTopics); 15410 function isFirstSave() { 15411 // Only return true if the user has not previous set prefs 15412 // and the selected topics pref is empty 15413 if (selectedTopics === "" && !topicsHaveBeenPreviouslySet) { 15414 return true; 15415 } 15416 return false; 15417 } 15418 function handleModalClose() { 15419 dispatch(actionCreators.OnlyToMain({ 15420 type: actionTypes.TOPIC_SELECTION_USER_DISMISS 15421 })); 15422 dispatch(actionCreators.BroadcastToContent({ 15423 type: actionTypes.TOPIC_SELECTION_SPOTLIGHT_CLOSE 15424 })); 15425 } 15426 function handleUserClose(e) { 15427 const id = e?.target?.id; 15428 if (id === "first-run") { 15429 dispatch(actionCreators.AlsoToMain({ 15430 type: actionTypes.TOPIC_SELECTION_MAYBE_LATER 15431 })); 15432 dispatch(actionCreators.SetPref("discoverystream.topicSelection.onboarding.maybeDisplay", true)); 15433 } else { 15434 dispatch(actionCreators.SetPref("discoverystream.topicSelection.onboarding.maybeDisplay", false)); 15435 } 15436 handleModalClose(); 15437 } 15438 15439 // By doing this, the useEffect that sets up the IntersectionObserver 15440 // will not re-run every time displayCount changes, 15441 // but the observer callback will always have access 15442 // to the latest displayCount value through the ref. 15443 (0,external_React_namespaceObject.useEffect)(() => { 15444 displayCountRef.current = displayCount; 15445 }, [displayCount]); 15446 (0,external_React_namespaceObject.useEffect)(() => { 15447 const { 15448 current 15449 } = modalRef; 15450 let observer; 15451 if (current) { 15452 observer = new IntersectionObserver(([entry]) => { 15453 if (entry.isIntersecting) { 15454 // if the user has seen the modal more than 3 times, 15455 // automatically remove them from onboarding 15456 if (displayCountRef.current > 3) { 15457 dispatch(actionCreators.SetPref("discoverystream.topicSelection.onboarding.maybeDisplay", false)); 15458 } 15459 observer.unobserve(modalRef.current); 15460 dispatch(actionCreators.AlsoToMain({ 15461 type: actionTypes.TOPIC_SELECTION_IMPRESSION 15462 })); 15463 } 15464 }); 15465 observer.observe(current); 15466 } 15467 return () => { 15468 if (current) { 15469 observer.unobserve(current); 15470 } 15471 }; 15472 }, [modalRef, dispatch]); 15473 15474 // when component mounts, set focus to input 15475 (0,external_React_namespaceObject.useEffect)(() => { 15476 inputRef?.current?.focus(); 15477 }, [inputRef]); 15478 const handleFocus = (0,external_React_namespaceObject.useCallback)(e => { 15479 // this list will have to be updated with other reusable components that get used inside of this modal 15480 const tabbableElements = modalRef.current.querySelectorAll('a[href], button, moz-button, input[tabindex="0"]'); 15481 const [firstTabableEl] = tabbableElements; 15482 const lastTabbableEl = tabbableElements[tabbableElements.length - 1]; 15483 let isTabPressed = e.key === "Tab" || e.keyCode === 9; 15484 let isArrowPressed = e.key === "ArrowUp" || e.key === "ArrowDown"; 15485 if (isTabPressed) { 15486 if (e.shiftKey) { 15487 if (document.activeElement === firstTabableEl) { 15488 lastTabbableEl.focus(); 15489 e.preventDefault(); 15490 } 15491 } else if (document.activeElement === lastTabbableEl) { 15492 firstTabableEl.focus(); 15493 e.preventDefault(); 15494 } 15495 } else if (isArrowPressed && checkboxWrapperRef.current.contains(document.activeElement)) { 15496 const checkboxElements = checkboxWrapperRef.current.querySelectorAll("input"); 15497 const [firstInput] = checkboxElements; 15498 const lastInput = checkboxElements[checkboxElements.length - 1]; 15499 const inputArr = Array.from(checkboxElements); 15500 const currentIndex = inputArr.indexOf(document.activeElement); 15501 let nextEl; 15502 if (e.key === "ArrowUp") { 15503 nextEl = document.activeElement === firstInput ? lastInput : checkboxElements[currentIndex - 1]; 15504 } else if (e.key === "ArrowDown") { 15505 nextEl = document.activeElement === lastInput ? firstInput : checkboxElements[currentIndex + 1]; 15506 } 15507 nextEl.tabIndex = 0; 15508 document.activeElement.tabIndex = -1; 15509 nextEl.focus(); 15510 } 15511 }, []); 15512 (0,external_React_namespaceObject.useEffect)(() => { 15513 const ref = modalRef.current; 15514 ref.addEventListener("keydown", handleFocus); 15515 inputRef.current.tabIndex = 0; 15516 return () => { 15517 ref.removeEventListener("keydown", handleFocus); 15518 }; 15519 }, [handleFocus]); 15520 function handleChange(e) { 15521 const topic = e.target.name; 15522 const isChecked = e.target.checked; 15523 if (isChecked) { 15524 setTopicsToSelect([...topicsToSelect, topic]); 15525 } else { 15526 const updatedTopics = topicsToSelect.filter(t => t !== topic); 15527 setTopicsToSelect(updatedTopics); 15528 } 15529 } 15530 function handleSubmit() { 15531 const topicsString = topicsToSelect.join(", "); 15532 dispatch(actionCreators.SetPref("discoverystream.topicSelection.selectedTopics", topicsString)); 15533 dispatch(actionCreators.SetPref("discoverystream.topicSelection.onboarding.maybeDisplay", false)); 15534 if (!topicsHaveBeenPreviouslySet) { 15535 dispatch(actionCreators.SetPref("discoverystream.topicSelection.hasBeenUpdatedPreviously", true)); 15536 } 15537 dispatch(actionCreators.OnlyToMain({ 15538 type: actionTypes.TOPIC_SELECTION_USER_SAVE, 15539 data: { 15540 topics: topicsString, 15541 previous_topics: selectedTopics, 15542 first_save: isFirstSave() 15543 } 15544 })); 15545 handleModalClose(); 15546 } 15547 return /*#__PURE__*/external_React_default().createElement(ModalOverlayWrapper, { 15548 onClose: handleUserClose, 15549 innerClassName: "topic-selection-container" 15550 }, /*#__PURE__*/external_React_default().createElement("div", { 15551 className: "topic-selection-form", 15552 ref: modalRef 15553 }, /*#__PURE__*/external_React_default().createElement("button", { 15554 className: "dismiss-button", 15555 title: "dismiss", 15556 onClick: handleUserClose 15557 }), /*#__PURE__*/external_React_default().createElement("h1", { 15558 className: "title", 15559 "data-l10n-id": "newtab-topic-selection-title" 15560 }), /*#__PURE__*/external_React_default().createElement("p", { 15561 className: "subtitle", 15562 "data-l10n-id": "newtab-topic-selection-subtitle" 15563 }), /*#__PURE__*/external_React_default().createElement("div", { 15564 className: "topic-list", 15565 ref: checkboxWrapperRef 15566 }, topics.map((topic, i) => { 15567 const checked = topicsToSelect.includes(topic); 15568 return /*#__PURE__*/external_React_default().createElement("label", { 15569 className: `topic-item`, 15570 key: topic 15571 }, /*#__PURE__*/external_React_default().createElement("input", { 15572 type: "checkbox", 15573 id: topic, 15574 name: topic, 15575 ref: i === 0 ? inputRef : null, 15576 onChange: handleChange, 15577 checked: checked, 15578 "aria-checked": checked, 15579 tabIndex: -1 15580 }), /*#__PURE__*/external_React_default().createElement("div", { 15581 className: `topic-custom-checkbox` 15582 }, /*#__PURE__*/external_React_default().createElement("span", { 15583 className: "topic-icon" 15584 }, EMOJI_LABELS[`${topic}`]), /*#__PURE__*/external_React_default().createElement("span", { 15585 className: "topic-checked" 15586 })), /*#__PURE__*/external_React_default().createElement("span", { 15587 className: "topic-item-label", 15588 "data-l10n-id": `newtab-topic-label-${topic}` 15589 })); 15590 })), /*#__PURE__*/external_React_default().createElement("div", { 15591 className: "modal-footer" 15592 }, /*#__PURE__*/external_React_default().createElement("a", { 15593 href: supportUrl, 15594 "data-l10n-id": "newtab-topic-selection-privacy-link" 15595 }), /*#__PURE__*/external_React_default().createElement("moz-button-group", { 15596 className: "button-group" 15597 }, /*#__PURE__*/external_React_default().createElement("moz-button", { 15598 id: isFirstRun ? "first-run" : "", 15599 "data-l10n-id": isFirstRun ? "newtab-topic-selection-button-maybe-later" : "newtab-topic-selection-cancel-button", 15600 onClick: handleUserClose 15601 }), /*#__PURE__*/external_React_default().createElement("moz-button", { 15602 "data-l10n-id": "newtab-topic-selection-save-button", 15603 type: "primary", 15604 onClick: handleSubmit 15605 }))))); 15606 } 15607 15608 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/FeatureHighlight/DownloadMobilePromoHighlight.jsx 15609 /* This Source Code Form is subject to the terms of the Mozilla Public 15610 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 15611 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 15612 15613 15614 15615 15616 15617 const PREF_MOBILE_DOWNLOAD_HIGHLIGHT_VARIANT_A = "mobileDownloadModal.variant-a"; 15618 const PREF_MOBILE_DOWNLOAD_HIGHLIGHT_VARIANT_B = "mobileDownloadModal.variant-b"; 15619 const PREF_MOBILE_DOWNLOAD_HIGHLIGHT_VARIANT_C = "mobileDownloadModal.variant-c"; 15620 const FEATURE_ID = "FEATURE_DOWNLOAD_MOBILE_PROMO"; 15621 function DownloadMobilePromoHighlight({ 15622 position, 15623 dispatch, 15624 handleDismiss, 15625 handleBlock, 15626 isIntersecting 15627 }) { 15628 const onDismiss = (0,external_React_namespaceObject.useCallback)(() => { 15629 // This event is emitted manually because the feature may be triggered outside the OMC flow, 15630 // and may not be captured by the messaging-system’s automatic reporting. 15631 dispatch(actionCreators.DiscoveryStreamUserEvent({ 15632 event: "FEATURE_HIGHLIGHT_DISMISS", 15633 source: "FEATURE_HIGHLIGHT", 15634 value: { 15635 feature: FEATURE_ID 15636 } 15637 })); 15638 handleDismiss(); 15639 handleBlock(); 15640 }, [dispatch, handleDismiss, handleBlock]); 15641 (0,external_React_namespaceObject.useEffect)(() => { 15642 if (isIntersecting) { 15643 // This event is emitted manually because the feature may be triggered outside the OMC flow, 15644 // and may not be captured by the messaging-system’s automatic reporting. 15645 dispatch(actionCreators.DiscoveryStreamUserEvent({ 15646 event: "FEATURE_HIGHLIGHT_IMPRESSION", 15647 source: "FEATURE_HIGHLIGHT", 15648 value: { 15649 feature: FEATURE_ID 15650 } 15651 })); 15652 } 15653 }, [dispatch, isIntersecting]); 15654 const prefs = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Prefs.values); 15655 const mobileDownloadPromoVarA = prefs[PREF_MOBILE_DOWNLOAD_HIGHLIGHT_VARIANT_A]; 15656 const mobileDownloadPromoVarB = prefs[PREF_MOBILE_DOWNLOAD_HIGHLIGHT_VARIANT_B]; 15657 const mobileDownloadPromoVarC = prefs[PREF_MOBILE_DOWNLOAD_HIGHLIGHT_VARIANT_C]; 15658 function getActiveVariant() { 15659 if (mobileDownloadPromoVarA) { 15660 return "A"; 15661 } 15662 if (mobileDownloadPromoVarB) { 15663 return "B"; 15664 } 15665 if (mobileDownloadPromoVarC) { 15666 return "C"; 15667 } 15668 return null; 15669 } 15670 function getVariantQRCodeImg() { 15671 const variant = getActiveVariant(); 15672 switch (variant) { 15673 case "A": 15674 return "chrome://newtab/content/data/content/assets/download-qr-code-var-a.png"; 15675 case "B": 15676 return "chrome://newtab/content/data/content/assets/download-qr-code-var-b.png"; 15677 case "C": 15678 return "chrome://newtab/content/data/content/assets/download-qr-code-var-c.png"; 15679 default: 15680 return null; 15681 } 15682 } 15683 function getVariantCopy() { 15684 const variant = getActiveVariant(); 15685 switch (variant) { 15686 case "A": 15687 return "newtab-download-mobile-highlight-body-variant-a"; 15688 case "B": 15689 return "newtab-download-mobile-highlight-body-variant-b"; 15690 case "C": 15691 return "newtab-download-mobile-highlight-body-variant-c"; 15692 default: 15693 return null; 15694 } 15695 } 15696 return /*#__PURE__*/external_React_default().createElement("div", { 15697 className: "download-firefox-feature-highlight" 15698 }, /*#__PURE__*/external_React_default().createElement(FeatureHighlight, { 15699 position: position, 15700 feature: FEATURE_ID, 15701 dispatch: dispatch, 15702 message: /*#__PURE__*/external_React_default().createElement("div", { 15703 className: "download-firefox-feature-highlight-content" 15704 }, /*#__PURE__*/external_React_default().createElement("img", { 15705 src: getVariantQRCodeImg(), 15706 "data-l10n-id": "newtab-download-mobile-highlight-image", 15707 width: "120", 15708 height: "191", 15709 alt: "" 15710 }), /*#__PURE__*/external_React_default().createElement("p", { 15711 className: "title", 15712 "data-l10n-id": "newtab-download-mobile-highlight-title" 15713 }), /*#__PURE__*/external_React_default().createElement("p", { 15714 className: "subtitle", 15715 "data-l10n-id": getVariantCopy() 15716 })), 15717 openedOverride: true, 15718 showButtonIcon: false, 15719 dismissCallback: onDismiss, 15720 outsideClickCallback: handleDismiss 15721 })); 15722 } 15723 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/FeatureHighlight/WallpaperFeatureHighlight.jsx 15724 /* This Source Code Form is subject to the terms of the Mozilla Public 15725 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 15726 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 15727 15728 15729 15730 15731 15732 function WallpaperFeatureHighlight({ 15733 position, 15734 dispatch, 15735 handleDismiss, 15736 handleClick, 15737 handleBlock 15738 }) { 15739 const onDismiss = (0,external_React_namespaceObject.useCallback)(() => { 15740 handleDismiss(); 15741 handleBlock(); 15742 }, [handleDismiss, handleBlock]); 15743 const onToggleClick = (0,external_React_namespaceObject.useCallback)(elementId => { 15744 dispatch({ 15745 type: actionTypes.SHOW_PERSONALIZE 15746 }); 15747 dispatch(actionCreators.UserEvent({ 15748 event: "SHOW_PERSONALIZE" 15749 })); 15750 handleClick(elementId); 15751 onDismiss(); 15752 }, [dispatch, onDismiss, handleClick]); 15753 15754 // Extract the strings and feature ID from OMC 15755 const { 15756 messageData 15757 } = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Messages); 15758 return /*#__PURE__*/external_React_default().createElement("div", { 15759 className: `wallpaper-feature-highlight ${messageData.content?.darkModeDismiss ? "is-inverted-dark-dismiss-button" : ""}` 15760 }, /*#__PURE__*/external_React_default().createElement(FeatureHighlight, { 15761 position: position, 15762 "data-l10n-id": "feature-highlight-wallpaper", 15763 feature: messageData.content.feature, 15764 dispatch: dispatch, 15765 message: /*#__PURE__*/external_React_default().createElement("div", { 15766 className: "wallpaper-feature-highlight-content" 15767 }, /*#__PURE__*/external_React_default().createElement("picture", { 15768 className: "follow-section-button-highlight-image" 15769 }, /*#__PURE__*/external_React_default().createElement("source", { 15770 srcSet: messageData.content?.darkModeImageURL || "chrome://newtab/content/data/content/assets/highlights/omc-newtab-wallpapers.svg", 15771 media: "(prefers-color-scheme: dark)" 15772 }), /*#__PURE__*/external_React_default().createElement("source", { 15773 srcSet: messageData.content?.imageURL || "chrome://newtab/content/data/content/assets/highlights/omc-newtab-wallpapers.svg", 15774 media: "(prefers-color-scheme: light)" 15775 }), /*#__PURE__*/external_React_default().createElement("img", { 15776 width: "320", 15777 height: "195", 15778 alt: "" 15779 })), messageData.content?.cardTitle ? /*#__PURE__*/external_React_default().createElement("p", { 15780 className: "title" 15781 }, messageData.content.cardTitle) : /*#__PURE__*/external_React_default().createElement("p", { 15782 className: "title", 15783 "data-l10n-id": messageData.content.title || "newtab-new-user-custom-wallpaper-title" 15784 }), messageData.content?.cardMessage ? /*#__PURE__*/external_React_default().createElement("p", { 15785 className: "subtitle" 15786 }, messageData.content.cardMessage) : /*#__PURE__*/external_React_default().createElement("p", { 15787 className: "subtitle", 15788 "data-l10n-id": messageData.content.subtitle || "newtab-new-user-custom-wallpaper-subtitle" 15789 }), /*#__PURE__*/external_React_default().createElement("span", { 15790 className: "button-wrapper" 15791 }, messageData.content?.cardCta ? /*#__PURE__*/external_React_default().createElement("moz-button", { 15792 type: "default", 15793 onClick: () => onToggleClick("open-customize-menu"), 15794 label: messageData.content.cardCta 15795 }) : /*#__PURE__*/external_React_default().createElement("moz-button", { 15796 type: "default", 15797 onClick: () => onToggleClick("open-customize-menu"), 15798 "data-l10n-id": messageData.content.cta || "newtab-new-user-custom-wallpaper-cta" 15799 }))), 15800 toggle: /*#__PURE__*/external_React_default().createElement("div", { 15801 className: "icon icon-help" 15802 }), 15803 openedOverride: true, 15804 showButtonIcon: false, 15805 dismissCallback: onDismiss, 15806 outsideClickCallback: handleDismiss 15807 })); 15808 } 15809 ;// CONCATENATED MODULE: ./content-src/components/Base/Base.jsx 15810 function Base_extends() { return Base_extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, Base_extends.apply(null, arguments); } 15811 /* This Source Code Form is subject to the terms of the Mozilla Public 15812 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 15813 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 15814 15815 15816 15817 15818 15819 15820 15821 15822 15823 15824 15825 15826 15827 15828 15829 15830 15831 15832 15833 15834 const Base_VISIBLE = "visible"; 15835 const Base_VISIBILITY_CHANGE_EVENT = "visibilitychange"; 15836 const PREF_INFERRED_PERSONALIZATION_SYSTEM = "discoverystream.sections.personalization.inferred.enabled"; 15837 const Base_PREF_INFERRED_PERSONALIZATION_USER = "discoverystream.sections.personalization.inferred.user.enabled"; 15838 15839 // Returns a function will not be continuously triggered when called. The 15840 // function will be triggered if called again after `wait` milliseconds. 15841 function Base_debounce(func, wait) { 15842 let timer; 15843 return (...args) => { 15844 if (timer) { 15845 return; 15846 } 15847 let wakeUp = () => { 15848 timer = null; 15849 }; 15850 timer = setTimeout(wakeUp, wait); 15851 func.apply(this, args); 15852 }; 15853 } 15854 function WithDsAdmin(props) { 15855 const { 15856 hash = globalThis?.location?.hash || "" 15857 } = props; 15858 const [devtoolsCollapsed, setDevtoolsCollapsed] = (0,external_React_namespaceObject.useState)(!hash.startsWith("#devtools")); 15859 (0,external_React_namespaceObject.useEffect)(() => { 15860 const onHashChange = () => { 15861 const h = globalThis?.location?.hash || ""; 15862 setDevtoolsCollapsed(!h.startsWith("#devtools")); 15863 }; 15864 15865 // run once in case hash changed before mount 15866 onHashChange(); 15867 globalThis?.addEventListener("hashchange", onHashChange); 15868 return () => globalThis?.removeEventListener("hashchange", onHashChange); 15869 }, []); 15870 return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement(DiscoveryStreamAdmin, { 15871 devtoolsCollapsed: devtoolsCollapsed 15872 }), devtoolsCollapsed ? /*#__PURE__*/external_React_default().createElement(BaseContent, props) : null); 15873 } 15874 function _Base(props) { 15875 const isDevtoolsEnabled = props.Prefs.values["asrouter.devtoolsEnabled"]; 15876 const { 15877 App 15878 } = props; 15879 if (!App.initialized) { 15880 return null; 15881 } 15882 return /*#__PURE__*/external_React_default().createElement(ErrorBoundary, { 15883 className: "base-content-fallback" 15884 }, isDevtoolsEnabled ? /*#__PURE__*/external_React_default().createElement(WithDsAdmin, props) : /*#__PURE__*/external_React_default().createElement(BaseContent, props)); 15885 } 15886 class BaseContent extends (external_React_default()).PureComponent { 15887 constructor(props) { 15888 super(props); 15889 this.openPreferences = this.openPreferences.bind(this); 15890 this.openCustomizationMenu = this.openCustomizationMenu.bind(this); 15891 this.closeCustomizationMenu = this.closeCustomizationMenu.bind(this); 15892 this.handleOnKeyDown = this.handleOnKeyDown.bind(this); 15893 this.onWindowScroll = Base_debounce(this.onWindowScroll.bind(this), 5); 15894 this.setPref = this.setPref.bind(this); 15895 this.shouldShowOMCHighlight = this.shouldShowOMCHighlight.bind(this); 15896 this.updateWallpaper = this.updateWallpaper.bind(this); 15897 this.prefersDarkQuery = null; 15898 this.handleColorModeChange = this.handleColorModeChange.bind(this); 15899 this.onVisible = this.onVisible.bind(this); 15900 this.toggleDownloadHighlight = this.toggleDownloadHighlight.bind(this); 15901 this.handleDismissDownloadHighlight = this.handleDismissDownloadHighlight.bind(this); 15902 this.applyBodyClasses = this.applyBodyClasses.bind(this); 15903 this.toggleSectionsMgmtPanel = this.toggleSectionsMgmtPanel.bind(this); 15904 this.state = { 15905 fixedSearch: false, 15906 firstVisibleTimestamp: null, 15907 colorMode: "", 15908 fixedNavStyle: {}, 15909 wallpaperTheme: "", 15910 showDownloadHighlightOverride: null, 15911 visible: false, 15912 showSectionsMgmtPanel: false 15913 }; 15914 this.spocPlaceholderStartTime = null; 15915 } 15916 setFirstVisibleTimestamp() { 15917 if (!this.state.firstVisibleTimestamp) { 15918 this.setState({ 15919 firstVisibleTimestamp: Date.now() 15920 }); 15921 } 15922 } 15923 onVisible() { 15924 this.setState({ 15925 visible: true 15926 }); 15927 this.setFirstVisibleTimestamp(); 15928 this.shouldDisplayTopicSelectionModal(); 15929 this.onVisibilityDispatch(); 15930 if (this.isSpocsOnDemandExpired && !this.spocPlaceholderStartTime) { 15931 this.spocPlaceholderStartTime = Date.now(); 15932 } 15933 } 15934 onVisibilityDispatch() { 15935 const { 15936 onDemand = {} 15937 } = this.props.DiscoveryStream.spocs; 15938 15939 // We only need to dispatch this if: 15940 // 1. onDemand is enabled, 15941 // 2. onDemand spocs have not been loaded on this tab. 15942 // 3. Spocs are expired. 15943 if (onDemand.enabled && !onDemand.loaded && this.isSpocsOnDemandExpired) { 15944 // This dispatches that spocs are expired and we need to update them. 15945 this.props.dispatch(actionCreators.OnlyToMain({ 15946 type: actionTypes.DISCOVERY_STREAM_SPOCS_ONDEMAND_UPDATE 15947 })); 15948 } 15949 } 15950 get isSpocsOnDemandExpired() { 15951 const { 15952 onDemand = {}, 15953 cacheUpdateTime, 15954 lastUpdated 15955 } = this.props.DiscoveryStream.spocs; 15956 15957 // We can bail early if: 15958 // 1. onDemand is off, 15959 // 2. onDemand spocs have been loaded on this tab. 15960 if (!onDemand.enabled || onDemand.loaded) { 15961 return false; 15962 } 15963 return Date.now() - lastUpdated >= cacheUpdateTime; 15964 } 15965 spocsOnDemandUpdated() { 15966 const { 15967 onDemand = {}, 15968 loaded 15969 } = this.props.DiscoveryStream.spocs; 15970 15971 // We only need to fire this if: 15972 // 1. Spoc data is loaded. 15973 // 2. onDemand is enabled. 15974 // 3. The component is visible (not preloaded tab). 15975 // 4. onDemand spocs have not been loaded on this tab. 15976 // 5. Spocs are not expired. 15977 if (loaded && onDemand.enabled && this.state.visible && !onDemand.loaded && !this.isSpocsOnDemandExpired) { 15978 // This dispatches that spocs have been loaded on this tab 15979 // and we don't need to update them again for this tab. 15980 this.props.dispatch(actionCreators.BroadcastToContent({ 15981 type: actionTypes.DISCOVERY_STREAM_SPOCS_ONDEMAND_LOAD 15982 })); 15983 } 15984 } 15985 componentDidMount() { 15986 this.applyBodyClasses(); 15987 __webpack_require__.g.addEventListener("scroll", this.onWindowScroll); 15988 __webpack_require__.g.addEventListener("keydown", this.handleOnKeyDown); 15989 const prefs = this.props.Prefs.values; 15990 const wallpapersEnabled = prefs["newtabWallpapers.enabled"]; 15991 if (!prefs["externalComponents.enabled"]) { 15992 if (prefs["search.useHandoffComponent"]) { 15993 // Dynamically import the contentSearchHandoffUI module, but don't worry 15994 // about webpacking this one. 15995 import(/* webpackIgnore: true */"chrome://browser/content/contentSearchHandoffUI.mjs"); 15996 } else { 15997 const scriptURL = "chrome://browser/content/contentSearchHandoffUI.js"; 15998 const scriptEl = document.createElement("script"); 15999 scriptEl.src = scriptURL; 16000 document.head.appendChild(scriptEl); 16001 } 16002 } 16003 if (this.props.document.visibilityState === Base_VISIBLE) { 16004 this.onVisible(); 16005 } else { 16006 this._onVisibilityChange = () => { 16007 if (this.props.document.visibilityState === Base_VISIBLE) { 16008 this.onVisible(); 16009 this.props.document.removeEventListener(Base_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 16010 this._onVisibilityChange = null; 16011 } 16012 }; 16013 this.props.document.addEventListener(Base_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 16014 } 16015 // track change event to dark/light mode 16016 this.prefersDarkQuery = globalThis.matchMedia("(prefers-color-scheme: dark)"); 16017 this.prefersDarkQuery.addEventListener("change", this.handleColorModeChange); 16018 this.handleColorModeChange(); 16019 if (wallpapersEnabled) { 16020 this.updateWallpaper(); 16021 } 16022 this._onHashChange = () => { 16023 const hash = globalThis.location?.hash || ""; 16024 if (hash === "#customize" || hash === "#customize-topics") { 16025 this.openCustomizationMenu(); 16026 if (hash === "#customize-topics") { 16027 this.toggleSectionsMgmtPanel(); 16028 } 16029 } else if (this.props.App.customizeMenuVisible) { 16030 this.closeCustomizationMenu(); 16031 } 16032 }; 16033 16034 // Using the Performance API to detect page reload vs fresh navigation. 16035 // Only open customize menu on fresh navigation, not on page refresh. 16036 // See: https://developer.mozilla.org/en-US/docs/Web/API/Performance/getEntriesByType 16037 // See: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/entryType#navigation 16038 // See: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming/type 16039 const isReload = globalThis.performance?.getEntriesByType("navigation")[0]?.type === "reload"; 16040 if (!isReload) { 16041 this._onHashChange(); 16042 } 16043 globalThis.addEventListener("hashchange", this._onHashChange); 16044 } 16045 componentDidUpdate(prevProps) { 16046 this.applyBodyClasses(); 16047 const prefs = this.props.Prefs.values; 16048 16049 // Check if weather widget was re-enabled from customization menu 16050 const wasWeatherDisabled = !prevProps.Prefs.values.showWeather; 16051 const isWeatherEnabled = this.props.Prefs.values.showWeather; 16052 if (wasWeatherDisabled && isWeatherEnabled) { 16053 // If weather widget was enabled from customization menu, display opt-in dialog 16054 this.props.dispatch(actionCreators.SetPref("weather.optInDisplayed", true)); 16055 } 16056 const wallpapersEnabled = prefs["newtabWallpapers.enabled"]; 16057 if (wallpapersEnabled) { 16058 // destructure current and previous props with fallbacks 16059 // (preventing undefined errors) 16060 const { 16061 Wallpapers: { 16062 uploadedWallpaper = null, 16063 wallpaperList = null 16064 } = {} 16065 } = this.props; 16066 const { 16067 Wallpapers: { 16068 uploadedWallpaper: prevUploadedWallpaper = null, 16069 wallpaperList: prevWallpaperList = null 16070 } = {}, 16071 Prefs: { 16072 values: prevPrefs = {} 16073 } = {} 16074 } = prevProps; 16075 const selectedWallpaper = prefs["newtabWallpapers.wallpaper"]; 16076 const prevSelectedWallpaper = prevPrefs["newtabWallpapers.wallpaper"]; 16077 const uploadedWallpaperTheme = prefs["newtabWallpapers.customWallpaper.theme"]; 16078 const prevUploadedWallpaperTheme = prevPrefs["newtabWallpapers.customWallpaper.theme"]; 16079 16080 // don't update wallpaper unless the wallpaper is being changed. 16081 if (selectedWallpaper !== prevSelectedWallpaper || 16082 // selecting a new wallpaper 16083 uploadedWallpaper !== prevUploadedWallpaper || 16084 // uploading a new wallpaper 16085 wallpaperList !== prevWallpaperList || 16086 // remote settings wallpaper list updates 16087 this.props.App.isForStartupCache.Wallpaper !== prevProps.App.isForStartupCache.Wallpaper || 16088 // Startup cached page wallpaper is updating 16089 uploadedWallpaperTheme !== prevUploadedWallpaperTheme) { 16090 this.updateWallpaper(); 16091 } 16092 } 16093 this.spocsOnDemandUpdated(); 16094 this.trackSpocPlaceholderDuration(prevProps); 16095 } 16096 trackSpocPlaceholderDuration(prevProps) { 16097 // isExpired returns true when the current props have expired spocs (showing placeholders) 16098 const isExpired = this.isSpocsOnDemandExpired; 16099 16100 // Init tracking when placeholders become visible 16101 if (isExpired && this.state.visible && !this.spocPlaceholderStartTime) { 16102 this.spocPlaceholderStartTime = Date.now(); 16103 } 16104 16105 // wasExpired returns true when the previous props had expired spocs (showing placeholders) 16106 const wasExpired = prevProps.DiscoveryStream.spocs.onDemand?.enabled && !prevProps.DiscoveryStream.spocs.onDemand?.loaded && Date.now() - prevProps.DiscoveryStream.spocs.lastUpdated >= prevProps.DiscoveryStream.spocs.cacheUpdateTime; 16107 16108 // Record duration telemetry event when placeholders are replaced with real content 16109 if (wasExpired && !isExpired && this.spocPlaceholderStartTime) { 16110 const duration = Date.now() - this.spocPlaceholderStartTime; 16111 this.props.dispatch(actionCreators.OnlyToMain({ 16112 type: actionTypes.DISCOVERY_STREAM_SPOC_PLACEHOLDER_DURATION, 16113 data: { 16114 duration 16115 } 16116 })); 16117 this.spocPlaceholderStartTime = null; 16118 } 16119 } 16120 handleColorModeChange() { 16121 const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; 16122 if (colorMode !== this.state.colorMode) { 16123 this.setState({ 16124 colorMode 16125 }); 16126 this.updateWallpaper(); 16127 } 16128 } 16129 componentWillUnmount() { 16130 this.prefersDarkQuery?.removeEventListener("change", this.handleColorModeChange); 16131 __webpack_require__.g.removeEventListener("scroll", this.onWindowScroll); 16132 __webpack_require__.g.removeEventListener("keydown", this.handleOnKeyDown); 16133 if (this._onVisibilityChange) { 16134 this.props.document.removeEventListener(Base_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); 16135 } 16136 if (this._onHashChange) { 16137 globalThis.removeEventListener("hashchange", this._onHashChange); 16138 } 16139 } 16140 onWindowScroll() { 16141 if (window.innerHeight <= 700) { 16142 // Bug 1937296: Only apply fixed-search logic 16143 // if the page is tall enough to support it. 16144 return; 16145 } 16146 const prefs = this.props.Prefs.values; 16147 const { 16148 showSearch 16149 } = prefs; 16150 if (!showSearch) { 16151 // Bug 1944718: Only apply fixed-search logic 16152 // if search is visible. 16153 return; 16154 } 16155 const logoAlwaysVisible = prefs["logowordmark.alwaysVisible"]; 16156 16157 /* Bug 1917937: The logic presented below is fragile but accurate to the pixel. As new tab experiments with layouts, we have a tech debt of competing styles and classes the slightly modify where the search bar sits on the page. The larger solution for this is to replace everything with an intersection observer, but would require a larger refactor of this file. In the interim, we can programmatically calculate when to fire the fixed-scroll event and account for the moved elements so that topsites/etc stays in the same place. The CSS this references has been flagged to reference this logic so (hopefully) keep them in sync. */ 16158 16159 let SCROLL_THRESHOLD = 0; // When the fixed-scroll event fires 16160 let MAIN_OFFSET_PADDING = 0; // The padding to compensate for the moved elements 16161 16162 const CSS_VAR_SPACE_XXLARGE = 32.04; // Custom Acorn themed variable (8 * 0.267rem); 16163 16164 let layout = { 16165 outerWrapperPaddingTop: 32.04, 16166 searchWrapperPaddingTop: 16.02, 16167 searchWrapperPaddingBottom: CSS_VAR_SPACE_XXLARGE, 16168 searchWrapperFixedScrollPaddingTop: 24.03, 16169 searchWrapperFixedScrollPaddingBottom: 24.03, 16170 searchInnerWrapperMinHeight: 52, 16171 logoAndWordmarkWrapperHeight: 0, 16172 logoAndWordmarkWrapperMarginBottom: 0 16173 }; 16174 16175 // Logo visibility applies to all layouts 16176 if (!logoAlwaysVisible) { 16177 layout.logoAndWordmarkWrapperHeight = 0; 16178 layout.logoAndWordmarkWrapperMarginBottom = 0; 16179 } 16180 SCROLL_THRESHOLD = layout.outerWrapperPaddingTop + layout.searchWrapperPaddingTop + layout.logoAndWordmarkWrapperHeight + layout.logoAndWordmarkWrapperMarginBottom - layout.searchWrapperFixedScrollPaddingTop; 16181 MAIN_OFFSET_PADDING = layout.searchWrapperPaddingTop + layout.searchWrapperPaddingBottom + layout.searchInnerWrapperMinHeight + layout.logoAndWordmarkWrapperHeight + layout.logoAndWordmarkWrapperMarginBottom; 16182 16183 // Edge case if logo and thums are turned off, but Var A is enabled 16184 if (SCROLL_THRESHOLD < 1) { 16185 SCROLL_THRESHOLD = 1; 16186 } 16187 if (__webpack_require__.g.scrollY > SCROLL_THRESHOLD && !this.state.fixedSearch) { 16188 this.setState({ 16189 fixedSearch: true, 16190 fixedNavStyle: { 16191 paddingBlockStart: `${MAIN_OFFSET_PADDING}px` 16192 } 16193 }); 16194 } else if (__webpack_require__.g.scrollY <= SCROLL_THRESHOLD && this.state.fixedSearch) { 16195 this.setState({ 16196 fixedSearch: false, 16197 fixedNavStyle: {} 16198 }); 16199 } 16200 } 16201 openPreferences() { 16202 this.props.dispatch(actionCreators.OnlyToMain({ 16203 type: actionTypes.SETTINGS_OPEN 16204 })); 16205 this.props.dispatch(actionCreators.UserEvent({ 16206 event: "OPEN_NEWTAB_PREFS" 16207 })); 16208 } 16209 openCustomizationMenu() { 16210 this.props.dispatch({ 16211 type: actionTypes.SHOW_PERSONALIZE 16212 }); 16213 this.props.dispatch(actionCreators.UserEvent({ 16214 event: "SHOW_PERSONALIZE" 16215 })); 16216 } 16217 closeCustomizationMenu() { 16218 if (this.props.App.customizeMenuVisible) { 16219 this.props.dispatch({ 16220 type: actionTypes.HIDE_PERSONALIZE 16221 }); 16222 this.props.dispatch(actionCreators.UserEvent({ 16223 event: "HIDE_PERSONALIZE" 16224 })); 16225 } 16226 } 16227 handleOnKeyDown(e) { 16228 if (e.key === "Escape") { 16229 this.closeCustomizationMenu(); 16230 } 16231 } 16232 setPref(pref, value) { 16233 this.props.dispatch(actionCreators.SetPref(pref, value)); 16234 } 16235 applyBodyClasses() { 16236 const { 16237 body 16238 } = this.props.document; 16239 if (!body) { 16240 return; 16241 } 16242 if (!body.classList.contains("activity-stream")) { 16243 body.classList.add("activity-stream"); 16244 } 16245 } 16246 renderWallpaperAttribution() { 16247 const { 16248 wallpaperList 16249 } = this.props.Wallpapers; 16250 const activeWallpaper = this.props.Prefs.values[`newtabWallpapers.wallpaper`]; 16251 const selected = wallpaperList.find(wp => wp.title === activeWallpaper); 16252 // make sure a wallpaper is selected and that the attribution also exists 16253 if (!selected?.attribution) { 16254 return null; 16255 } 16256 const { 16257 name: authorDetails, 16258 webpage 16259 } = selected.attribution; 16260 if (activeWallpaper && wallpaperList && authorDetails.url) { 16261 return /*#__PURE__*/external_React_default().createElement("p", { 16262 className: `wallpaper-attribution`, 16263 key: authorDetails.string, 16264 "data-l10n-id": "newtab-wallpaper-attribution", 16265 "data-l10n-args": JSON.stringify({ 16266 author_string: authorDetails.string, 16267 author_url: authorDetails.url, 16268 webpage_string: webpage.string, 16269 webpage_url: webpage.url 16270 }) 16271 }, /*#__PURE__*/external_React_default().createElement("a", { 16272 "data-l10n-name": "name-link", 16273 href: authorDetails.url 16274 }, authorDetails.string), /*#__PURE__*/external_React_default().createElement("a", { 16275 "data-l10n-name": "webpage-link", 16276 href: webpage.url 16277 }, webpage.string)); 16278 } 16279 return null; 16280 } 16281 async updateWallpaper() { 16282 const prefs = this.props.Prefs.values; 16283 const selectedWallpaper = prefs["newtabWallpapers.wallpaper"]; 16284 const { 16285 wallpaperList, 16286 uploadedWallpaper: uploadedWallpaperUrl 16287 } = this.props.Wallpapers; 16288 const uploadedWallpaperTheme = prefs["newtabWallpapers.customWallpaper.theme"]; 16289 // Uuse this.prefersDarkQuery since this.state.colorMode can be undefined when this is called 16290 const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; 16291 let url = ""; 16292 let color = "transparent"; 16293 let newTheme = colorMode; 16294 let backgroundPosition = "center"; 16295 16296 // if no selected wallpaper fallback to browser/theme styles 16297 if (!selectedWallpaper) { 16298 __webpack_require__.g.document?.body.style.removeProperty("--newtab-wallpaper"); 16299 __webpack_require__.g.document?.body.style.removeProperty("--newtab-wallpaper-color"); 16300 __webpack_require__.g.document?.body.style.removeProperty("--newtab-wallpaper-backgroundPosition"); 16301 __webpack_require__.g.document?.body.classList.remove("lightWallpaper", "darkWallpaper"); 16302 return; 16303 } 16304 16305 // uploaded wallpaper 16306 if (selectedWallpaper === "custom" && uploadedWallpaperUrl) { 16307 url = uploadedWallpaperUrl; 16308 color = "transparent"; 16309 // Note: There is no method to set a specific background position for custom wallpapers 16310 backgroundPosition = "center"; 16311 newTheme = uploadedWallpaperTheme || colorMode; 16312 } else if (wallpaperList) { 16313 const wallpaper = wallpaperList.find(wp => wp.title === selectedWallpaper); 16314 // solid color picker 16315 if (selectedWallpaper.includes("solid-color-picker")) { 16316 const regexRGB = /#([a-fA-F0-9]{6})/; 16317 const hex = selectedWallpaper.match(regexRGB)?.[0]; 16318 url = ""; 16319 color = hex; 16320 const rgbColors = this.getRGBColors(hex); 16321 newTheme = this.isWallpaperColorDark(rgbColors) ? "dark" : "light"; 16322 // standard wallpaper & solid colors 16323 } else if (selectedWallpaper) { 16324 url = wallpaper?.wallpaperUrl || ""; 16325 backgroundPosition = wallpaper?.background_position || "center"; 16326 color = wallpaper?.solid_color || "transparent"; 16327 newTheme = wallpaper?.theme || colorMode; 16328 // if a solid color, determine if dark or light 16329 if (wallpaper?.solid_color) { 16330 const rgbColors = this.getRGBColors(wallpaper.solid_color); 16331 const isColorDark = this.isWallpaperColorDark(rgbColors); 16332 newTheme = isColorDark ? "dark" : "light"; 16333 } 16334 } 16335 } 16336 __webpack_require__.g.document?.body.style.setProperty("--newtab-wallpaper", `url(${url})`); 16337 __webpack_require__.g.document?.body.style.setProperty("--newtab-wallpaper-backgroundPosition", backgroundPosition); 16338 __webpack_require__.g.document?.body.style.setProperty("--newtab-wallpaper-color", color || "transparent"); 16339 __webpack_require__.g.document?.body.classList.remove("lightWallpaper", "darkWallpaper"); 16340 __webpack_require__.g.document?.body.classList.add(newTheme === "dark" ? "darkWallpaper" : "lightWallpaper"); 16341 } 16342 shouldShowOMCHighlight(componentId) { 16343 const messageData = this.props.Messages?.messageData; 16344 if (!messageData || Object.keys(messageData).length === 0) { 16345 return false; 16346 } 16347 return messageData?.content?.messageType === componentId; 16348 } 16349 toggleDownloadHighlight() { 16350 this.setState(prevState => { 16351 const override = !(prevState.showDownloadHighlightOverride ?? this.shouldShowOMCHighlight("DownloadMobilePromoHighlight")); 16352 if (override) { 16353 // Emit an open event manually since OMC isn't handling it 16354 this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({ 16355 event: "FEATURE_HIGHLIGHT_OPEN", 16356 source: "FEATURE_HIGHLIGHT", 16357 value: { 16358 feature: "FEATURE_DOWNLOAD_MOBILE_PROMO" 16359 } 16360 })); 16361 } 16362 return { 16363 showDownloadHighlightOverride: override 16364 }; 16365 }); 16366 } 16367 handleDismissDownloadHighlight() { 16368 this.setState({ 16369 showDownloadHighlightOverride: false 16370 }); 16371 } 16372 getRGBColors(input) { 16373 if (input.length !== 7) { 16374 return []; 16375 } 16376 const r = parseInt(input.substr(1, 2), 16); 16377 const g = parseInt(input.substr(3, 2), 16); 16378 const b = parseInt(input.substr(5, 2), 16); 16379 return [r, g, b]; 16380 } 16381 isWallpaperColorDark([r, g, b]) { 16382 return 0.2125 * r + 0.7154 * g + 0.0721 * b <= 110; 16383 } 16384 toggleSectionsMgmtPanel() { 16385 this.setState(prevState => ({ 16386 showSectionsMgmtPanel: !prevState.showSectionsMgmtPanel 16387 })); 16388 } 16389 shouldDisplayTopicSelectionModal() { 16390 const prefs = this.props.Prefs.values; 16391 const pocketEnabled = prefs["feeds.section.topstories"] && prefs["feeds.system.topstories"]; 16392 const topicSelectionOnboardingEnabled = prefs["discoverystream.topicSelection.onboarding.enabled"] && pocketEnabled; 16393 const maybeShowModal = prefs["discoverystream.topicSelection.onboarding.maybeDisplay"]; 16394 const displayTimeout = prefs["discoverystream.topicSelection.onboarding.displayTimeout"]; 16395 const lastDisplayed = prefs["discoverystream.topicSelection.onboarding.lastDisplayed"]; 16396 const displayCount = prefs["discoverystream.topicSelection.onboarding.displayCount"]; 16397 if (!maybeShowModal || !prefs["discoverystream.topicSelection.enabled"] || !topicSelectionOnboardingEnabled) { 16398 return; 16399 } 16400 const day = 24 * 60 * 60 * 1000; 16401 const now = new Date().getTime(); 16402 const timeoutOccured = now - parseFloat(lastDisplayed) > displayTimeout; 16403 if (displayCount < 3) { 16404 if (displayCount === 0 || timeoutOccured) { 16405 this.props.dispatch(actionCreators.BroadcastToContent({ 16406 type: actionTypes.TOPIC_SELECTION_SPOTLIGHT_OPEN 16407 })); 16408 this.setPref("discoverystream.topicSelection.onboarding.displayTimeout", day); 16409 } 16410 } 16411 } 16412 16413 // eslint-disable-next-line max-statements, complexity 16414 render() { 16415 const { 16416 props 16417 } = this; 16418 const { 16419 App, 16420 DiscoveryStream 16421 } = props; 16422 const { 16423 initialized, 16424 customizeMenuVisible 16425 } = App; 16426 const prefs = props.Prefs.values; 16427 const activeWallpaper = prefs[`newtabWallpapers.wallpaper`]; 16428 const wallpapersEnabled = prefs["newtabWallpapers.enabled"]; 16429 const weatherEnabled = prefs.showWeather; 16430 const { 16431 showTopicSelection 16432 } = DiscoveryStream; 16433 const mayShowTopicSelection = showTopicSelection && prefs["discoverystream.topicSelection.enabled"]; 16434 const isDiscoveryStream = props.DiscoveryStream.config && props.DiscoveryStream.config.enabled; 16435 let filteredSections = props.Sections.filter(section => section.id !== "topstories"); 16436 const pocketEnabled = prefs["feeds.section.topstories"] && prefs["feeds.system.topstories"]; 16437 const noSectionsEnabled = !prefs["feeds.topsites"] && !pocketEnabled && filteredSections.filter(section => section.enabled).length === 0; 16438 const enabledSections = { 16439 topSitesEnabled: prefs["feeds.topsites"], 16440 pocketEnabled: prefs["feeds.section.topstories"], 16441 showInferredPersonalizationEnabled: prefs[Base_PREF_INFERRED_PERSONALIZATION_USER], 16442 topSitesRowsCount: prefs.topSitesRows, 16443 weatherEnabled: prefs.showWeather 16444 }; 16445 const pocketRegion = prefs["feeds.system.topstories"]; 16446 const mayHaveInferredPersonalization = prefs[PREF_INFERRED_PERSONALIZATION_SYSTEM]; 16447 const mayHaveWeather = prefs["system.showWeather"] || prefs.trainhopConfig?.weather?.enabled; 16448 const supportUrl = prefs["support.url"]; 16449 16450 // Weather can be enabled and not rendered in the top right corner 16451 const shouldDisplayWeather = prefs.showWeather && this.props.weatherPlacement === "header"; 16452 16453 // Widgets experiment pref check 16454 const nimbusWidgetsEnabled = prefs.widgetsConfig?.enabled; 16455 const nimbusListsEnabled = prefs.widgetsConfig?.listsEnabled; 16456 const nimbusTimerEnabled = prefs.widgetsConfig?.timerEnabled; 16457 const nimbusWidgetsTrainhopEnabled = prefs.trainhopConfig?.widgets?.enabled; 16458 const nimbusListsTrainhopEnabled = prefs.trainhopConfig?.widgets?.listsEnabled; 16459 const nimbusTimerTrainhopEnabled = prefs.trainhopConfig?.widgets?.timerEnabled; 16460 const mayHaveWidgets = prefs["widgets.system.enabled"] || nimbusWidgetsEnabled || nimbusWidgetsTrainhopEnabled; 16461 const mayHaveListsWidget = prefs["widgets.system.lists.enabled"] || nimbusListsEnabled || nimbusListsTrainhopEnabled; 16462 const mayHaveTimerWidget = prefs["widgets.system.focusTimer.enabled"] || nimbusTimerEnabled || nimbusTimerTrainhopEnabled; 16463 16464 // These prefs set the initial values on the Customize panel toggle switches 16465 const enabledWidgets = { 16466 listsEnabled: prefs["widgets.lists.enabled"], 16467 timerEnabled: prefs["widgets.focusTimer.enabled"], 16468 weatherEnabled: prefs.showWeather 16469 }; 16470 16471 // Mobile Download Promo Pref Checks 16472 const mobileDownloadPromoEnabled = prefs["mobileDownloadModal.enabled"]; 16473 const mobileDownloadPromoVariantAEnabled = prefs["mobileDownloadModal.variant-a"]; 16474 const mobileDownloadPromoVariantBEnabled = prefs["mobileDownloadModal.variant-b"]; 16475 const mobileDownloadPromoVariantCEnabled = prefs["mobileDownloadModal.variant-c"]; 16476 const mobileDownloadPromoVariantABorC = mobileDownloadPromoVariantAEnabled || mobileDownloadPromoVariantBEnabled || mobileDownloadPromoVariantCEnabled; 16477 const mobileDownloadPromoWrapperHeightModifier = prefs["weather.display"] === "detailed" && weatherEnabled && shouldDisplayWeather && mayHaveWeather ? "is-tall" : ""; 16478 const sectionsEnabled = prefs["discoverystream.sections.enabled"]; 16479 const topicLabelsEnabled = prefs["discoverystream.topicLabels.enabled"]; 16480 const sectionsCustomizeMenuPanelEnabled = prefs["discoverystream.sections.customizeMenuPanel.enabled"]; 16481 const sectionsPersonalizationEnabled = prefs["discoverystream.sections.personalization.enabled"]; 16482 16483 // Logic to show follow/block topic mgmt panel in Customize panel 16484 const mayHavePersonalizedTopicSections = sectionsPersonalizationEnabled && topicLabelsEnabled && sectionsEnabled && sectionsCustomizeMenuPanelEnabled && DiscoveryStream.feeds.loaded; 16485 const featureClassName = [mobileDownloadPromoEnabled && mobileDownloadPromoVariantABorC && "has-mobile-download-promo", 16486 // Mobile download promo modal is enabled/visible 16487 weatherEnabled && mayHaveWeather && shouldDisplayWeather && "has-weather", 16488 // Weather widget is enabled/visible 16489 prefs.showSearch ? "has-search" : "no-search", 16490 // layoutsVariantAEnabled ? "layout-variant-a" : "", // Layout experiment variant A 16491 // layoutsVariantBEnabled ? "layout-variant-b" : "", // Layout experiment variant B 16492 pocketEnabled ? "has-recommended-stories" : "no-recommended-stories", sectionsEnabled ? "has-sections-grid" : ""].filter(v => v).join(" "); 16493 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["feeds.topsites"] && !pocketEnabled && !prefs.showSearch && "only-topsites", noSectionsEnabled && "no-sections", prefs["logowordmark.alwaysVisible"] && "visible-logo"].filter(v => v).join(" "); 16494 16495 // If state.showDownloadHighlightOverride has value, let it override the logic 16496 // Otherwise, defer to OMC message display logic 16497 const shouldShowDownloadHighlight = this.state.showDownloadHighlightOverride ?? this.shouldShowOMCHighlight("DownloadMobilePromoHighlight"); 16498 return /*#__PURE__*/external_React_default().createElement("div", { 16499 className: featureClassName 16500 }, /*#__PURE__*/external_React_default().createElement("div", { 16501 className: "weatherWrapper" 16502 }, shouldDisplayWeather && /*#__PURE__*/external_React_default().createElement(ErrorBoundary, null, /*#__PURE__*/external_React_default().createElement(Weather_Weather, null))), /*#__PURE__*/external_React_default().createElement("div", { 16503 className: `mobileDownloadPromoWrapper ${mobileDownloadPromoWrapperHeightModifier}` 16504 }, mobileDownloadPromoEnabled && mobileDownloadPromoVariantABorC && /*#__PURE__*/external_React_default().createElement(ErrorBoundary, null, /*#__PURE__*/external_React_default().createElement(DownloadModalToggle, { 16505 isActive: shouldShowDownloadHighlight, 16506 onClick: this.toggleDownloadHighlight 16507 }), shouldShowDownloadHighlight && /*#__PURE__*/external_React_default().createElement(MessageWrapper, { 16508 hiddenOverride: shouldShowDownloadHighlight, 16509 onDismiss: this.handleDismissDownloadHighlight, 16510 dispatch: this.props.dispatch 16511 }, /*#__PURE__*/external_React_default().createElement(DownloadMobilePromoHighlight, { 16512 position: `inset-inline-start inset-block-end`, 16513 dispatch: this.props.dispatch 16514 })))), /*#__PURE__*/external_React_default().createElement("div", { 16515 className: outerClassName, 16516 onClick: this.closeCustomizationMenu 16517 }, /*#__PURE__*/external_React_default().createElement("main", { 16518 className: "newtab-main", 16519 style: this.state.fixedNavStyle 16520 }, prefs.showSearch && /*#__PURE__*/external_React_default().createElement("div", { 16521 className: "non-collapsible-section" 16522 }, /*#__PURE__*/external_React_default().createElement(ErrorBoundary, null, /*#__PURE__*/external_React_default().createElement(Search_Search, Base_extends({ 16523 showLogo: noSectionsEnabled || prefs["logowordmark.alwaysVisible"] 16524 }, props.Search)))), !prefs.showSearch && !noSectionsEnabled && /*#__PURE__*/external_React_default().createElement(Logo, null), /*#__PURE__*/external_React_default().createElement("div", { 16525 className: `body-wrapper${initialized ? " on" : ""}` 16526 }, isDiscoveryStream ? /*#__PURE__*/external_React_default().createElement(ErrorBoundary, { 16527 className: "borderless-error" 16528 }, /*#__PURE__*/external_React_default().createElement(DiscoveryStreamBase, { 16529 locale: props.App.locale, 16530 firstVisibleTimestamp: this.state.firstVisibleTimestamp, 16531 placeholder: this.isSpocsOnDemandExpired 16532 })) : /*#__PURE__*/external_React_default().createElement(Sections_Sections, null)), /*#__PURE__*/external_React_default().createElement(ConfirmDialog, null), wallpapersEnabled && this.renderWallpaperAttribution()), /*#__PURE__*/external_React_default().createElement("aside", null, this.props.Notifications?.showNotifications && /*#__PURE__*/external_React_default().createElement(ErrorBoundary, null, /*#__PURE__*/external_React_default().createElement(Notifications_Notifications, { 16533 dispatch: this.props.dispatch 16534 }))), mayShowTopicSelection && pocketEnabled && /*#__PURE__*/external_React_default().createElement(TopicSelection, { 16535 supportUrl: supportUrl 16536 })), /*#__PURE__*/external_React_default().createElement("menu", { 16537 className: "personalizeButtonWrapper" 16538 }, /*#__PURE__*/external_React_default().createElement(CustomizeMenu, { 16539 onClose: this.closeCustomizationMenu, 16540 onOpen: this.openCustomizationMenu, 16541 openPreferences: this.openPreferences, 16542 setPref: this.setPref, 16543 enabledSections: enabledSections, 16544 enabledWidgets: enabledWidgets, 16545 wallpapersEnabled: wallpapersEnabled, 16546 activeWallpaper: activeWallpaper, 16547 pocketRegion: pocketRegion, 16548 mayHaveTopicSections: mayHavePersonalizedTopicSections, 16549 mayHaveInferredPersonalization: mayHaveInferredPersonalization, 16550 mayHaveWeather: mayHaveWeather, 16551 mayHaveWidgets: mayHaveWidgets, 16552 mayHaveTimerWidget: mayHaveTimerWidget, 16553 mayHaveListsWidget: mayHaveListsWidget, 16554 showing: customizeMenuVisible, 16555 toggleSectionsMgmtPanel: this.toggleSectionsMgmtPanel, 16556 showSectionsMgmtPanel: this.state.showSectionsMgmtPanel 16557 }), this.shouldShowOMCHighlight("CustomWallpaperHighlight") && /*#__PURE__*/external_React_default().createElement(MessageWrapper, { 16558 dispatch: this.props.dispatch 16559 }, /*#__PURE__*/external_React_default().createElement(WallpaperFeatureHighlight, { 16560 position: "inset-block-start inset-inline-start", 16561 dispatch: this.props.dispatch 16562 })))); 16563 } 16564 } 16565 BaseContent.defaultProps = { 16566 document: __webpack_require__.g.document 16567 }; 16568 const Base = (0,external_ReactRedux_namespaceObject.connect)(state => ({ 16569 App: state.App, 16570 Prefs: state.Prefs, 16571 Sections: state.Sections, 16572 DiscoveryStream: state.DiscoveryStream, 16573 Messages: state.Messages, 16574 Notifications: state.Notifications, 16575 Search: state.Search, 16576 Wallpapers: state.Wallpapers, 16577 Weather: state.Weather, 16578 weatherPlacement: selectWeatherPlacement(state) 16579 }))(_Base); 16580 ;// CONCATENATED MODULE: ./content-src/lib/detect-user-session-start.mjs 16581 /* This Source Code Form is subject to the terms of the Mozilla Public 16582 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 16583 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 16584 16585 16586 16587 16588 const detect_user_session_start_VISIBLE = "visible"; 16589 const detect_user_session_start_VISIBILITY_CHANGE_EVENT = "visibilitychange"; 16590 16591 class DetectUserSessionStart { 16592 constructor(store, options = {}) { 16593 this._store = store; 16594 // Overrides for testing 16595 this.document = options.document || globalThis.document; 16596 this._perfService = options.perfService || perfService; 16597 this._onVisibilityChange = this._onVisibilityChange.bind(this); 16598 } 16599 16600 /** 16601 * sendEventOrAddListener - Notify immediately if the page is already visible, 16602 * or else set up a listener for when visibility changes. 16603 * This is needed for accurate session tracking for telemetry, 16604 * because tabs are pre-loaded. 16605 */ 16606 sendEventOrAddListener() { 16607 if (this.document.visibilityState === detect_user_session_start_VISIBLE) { 16608 // If the document is already visible, to the user, send a notification 16609 // immediately that a session has started. 16610 this._sendEvent(); 16611 } else { 16612 // If the document is not visible, listen for when it does become visible. 16613 this.document.addEventListener( 16614 detect_user_session_start_VISIBILITY_CHANGE_EVENT, 16615 this._onVisibilityChange 16616 ); 16617 } 16618 } 16619 16620 /** 16621 * _sendEvent - Sends a message to the main process to indicate the current 16622 * tab is now visible to the user, includes the 16623 * visibility_event_rcvd_ts time in ms from the UNIX epoch. 16624 */ 16625 _sendEvent() { 16626 this._perfService.mark("visibility_event_rcvd_ts"); 16627 16628 try { 16629 let visibility_event_rcvd_ts = 16630 this._perfService.getMostRecentAbsMarkStartByName( 16631 "visibility_event_rcvd_ts" 16632 ); 16633 16634 this._store.dispatch( 16635 actionCreators.AlsoToMain({ 16636 type: actionTypes.SAVE_SESSION_PERF_DATA, 16637 data: { 16638 visibility_event_rcvd_ts, 16639 window_inner_width: window.innerWidth, 16640 window_inner_height: window.innerHeight, 16641 }, 16642 }) 16643 ); 16644 } catch (ex) { 16645 // If this failed, it's likely because the `privacy.resistFingerprinting` 16646 // pref is true. We should at least not blow up. 16647 } 16648 } 16649 16650 /** 16651 * _onVisibilityChange - If the visibility has changed to visible, sends a notification 16652 * and removes the event listener. This should only be called once per tab. 16653 */ 16654 _onVisibilityChange() { 16655 if (this.document.visibilityState === detect_user_session_start_VISIBLE) { 16656 this._sendEvent(); 16657 this.document.removeEventListener( 16658 detect_user_session_start_VISIBILITY_CHANGE_EVENT, 16659 this._onVisibilityChange 16660 ); 16661 } 16662 } 16663 } 16664 16665 ;// CONCATENATED MODULE: external "Redux" 16666 const external_Redux_namespaceObject = Redux; 16667 ;// CONCATENATED MODULE: ./content-src/lib/init-store.mjs 16668 /* This Source Code Form is subject to the terms of the Mozilla Public 16669 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 16670 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 16671 16672 16673 // We disable import checking here as redux is installed via the npm packages 16674 // at the newtab level, rather than in the top-level package.json. 16675 // eslint-disable-next-line import/no-unresolved 16676 16677 16678 const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE"; 16679 const OUTGOING_MESSAGE_NAME = "ActivityStream:ContentToMain"; 16680 const INCOMING_MESSAGE_NAME = "ActivityStream:MainToContent"; 16681 16682 /** 16683 * A higher-order function which returns a reducer that, on MERGE_STORE action, 16684 * will return the action.data object merged into the previous state. 16685 * 16686 * For all other actions, it merely calls mainReducer. 16687 * 16688 * Because we want this to merge the entire state object, it's written as a 16689 * higher order function which takes the main reducer (itself often a call to 16690 * combineReducers) as a parameter. 16691 * 16692 * @param {function} mainReducer reducer to call if action != MERGE_STORE_ACTION 16693 * @return {function} a reducer that, on MERGE_STORE_ACTION action, 16694 * will return the action.data object merged 16695 * into the previous state, and the result 16696 * of calling mainReducer otherwise. 16697 */ 16698 function mergeStateReducer(mainReducer) { 16699 return (prevState, action) => { 16700 if (action.type === MERGE_STORE_ACTION) { 16701 return { ...prevState, ...action.data }; 16702 } 16703 16704 return mainReducer(prevState, action); 16705 }; 16706 } 16707 16708 /** 16709 * messageMiddleware - Middleware that looks for SentToMain type actions, and sends them if necessary 16710 */ 16711 const messageMiddleware = () => next => action => { 16712 const skipLocal = action.meta && action.meta.skipLocal; 16713 if (actionUtils.isSendToMain(action)) { 16714 RPMSendAsyncMessage(OUTGOING_MESSAGE_NAME, action); 16715 } 16716 if (!skipLocal) { 16717 next(action); 16718 } 16719 }; 16720 16721 const rehydrationMiddleware = ({ getState }) => { 16722 // NB: The parameter here is MiddlewareAPI which looks like a Store and shares 16723 // the same getState, so attached properties are accessible from the store. 16724 getState.didRehydrate = false; 16725 getState.didRequestInitialState = false; 16726 return next => action => { 16727 if (getState.didRehydrate || window.__FROM_STARTUP_CACHE__) { 16728 // Startup messages can be safely ignored by the about:home document 16729 // stored in the startup cache. 16730 if ( 16731 window.__FROM_STARTUP_CACHE__ && 16732 action.meta && 16733 action.meta.isStartup 16734 ) { 16735 return null; 16736 } 16737 return next(action); 16738 } 16739 16740 const isMergeStoreAction = action.type === MERGE_STORE_ACTION; 16741 const isRehydrationRequest = action.type === actionTypes.NEW_TAB_STATE_REQUEST; 16742 16743 if (isRehydrationRequest) { 16744 getState.didRequestInitialState = true; 16745 return next(action); 16746 } 16747 16748 if (isMergeStoreAction) { 16749 getState.didRehydrate = true; 16750 return next(action); 16751 } 16752 16753 // If init happened after our request was made, we need to re-request 16754 if (getState.didRequestInitialState && action.type === actionTypes.INIT) { 16755 return next(actionCreators.AlsoToMain({ type: actionTypes.NEW_TAB_STATE_REQUEST })); 16756 } 16757 16758 if ( 16759 actionUtils.isBroadcastToContent(action) || 16760 actionUtils.isSendToOneContent(action) || 16761 actionUtils.isSendToPreloaded(action) 16762 ) { 16763 // Note that actions received before didRehydrate will not be dispatched 16764 // because this could negatively affect preloading and the the state 16765 // will be replaced by rehydration anyway. 16766 return null; 16767 } 16768 16769 return next(action); 16770 }; 16771 }; 16772 16773 /** 16774 * initStore - Create a store and listen for incoming actions 16775 * 16776 * @param {object} reducers An object containing Redux reducers 16777 * @param {object} intialState (optional) The initial state of the store, if desired 16778 * @return {object} A redux store 16779 */ 16780 function initStore(reducers, initialState) { 16781 const store = (0,external_Redux_namespaceObject.createStore)( 16782 mergeStateReducer((0,external_Redux_namespaceObject.combineReducers)(reducers)), 16783 initialState, 16784 globalThis.RPMAddMessageListener && 16785 (0,external_Redux_namespaceObject.applyMiddleware)(rehydrationMiddleware, messageMiddleware) 16786 ); 16787 16788 if (globalThis.RPMAddMessageListener) { 16789 globalThis.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => { 16790 try { 16791 store.dispatch(msg.data); 16792 } catch (ex) { 16793 console.error("Content msg:", msg, "Dispatch error: ", ex); 16794 dump( 16795 `Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${ 16796 ex.stack 16797 }` 16798 ); 16799 } 16800 }); 16801 } 16802 16803 return store; 16804 } 16805 16806 ;// CONCATENATED MODULE: external "ReactDOM" 16807 const external_ReactDOM_namespaceObject = ReactDOM; 16808 var external_ReactDOM_default = /*#__PURE__*/__webpack_require__.n(external_ReactDOM_namespaceObject); 16809 ;// CONCATENATED MODULE: ./content-src/activity-stream.jsx 16810 /* This Source Code Form is subject to the terms of the Mozilla Public 16811 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 16812 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 16813 16814 16815 16816 16817 16818 16819 16820 16821 16822 const NewTab = ({ 16823 store 16824 }) => /*#__PURE__*/external_React_default().createElement(external_ReactRedux_namespaceObject.Provider, { 16825 store: store 16826 }, /*#__PURE__*/external_React_default().createElement(Base, null)); 16827 function doRequestWhenReady() { 16828 // If this document has already gone into the background by the time we've reached 16829 // here, we can deprioritize the request until the event loop 16830 // frees up. If, however, the visibility changes, we then send the request. 16831 const doRequestPromise = new Promise(resolve => { 16832 let didRequest = false; 16833 let requestIdleCallbackId = 0; 16834 function doRequest() { 16835 if (!didRequest) { 16836 if (requestIdleCallbackId) { 16837 cancelIdleCallback(requestIdleCallbackId); 16838 } 16839 didRequest = true; 16840 resolve(); 16841 } 16842 } 16843 if (document.hidden) { 16844 requestIdleCallbackId = requestIdleCallback(doRequest); 16845 addEventListener("visibilitychange", doRequest, { 16846 once: true 16847 }); 16848 } else { 16849 resolve(); 16850 } 16851 }); 16852 return doRequestPromise; 16853 } 16854 function renderWithoutState() { 16855 const store = initStore(reducers); 16856 new DetectUserSessionStart(store).sendEventOrAddListener(); 16857 doRequestWhenReady().then(() => { 16858 // If state events happened before we got here, we can request state again. 16859 store.dispatch(actionCreators.AlsoToMain({ 16860 type: actionTypes.NEW_TAB_STATE_REQUEST 16861 })); 16862 // If we rendered without state, we don't need the startup cache. 16863 store.dispatch(actionCreators.OnlyToMain({ 16864 type: actionTypes.NEW_TAB_STATE_REQUEST_WITHOUT_STARTUPCACHE 16865 })); 16866 }); 16867 external_ReactDOM_default().hydrate(/*#__PURE__*/external_React_default().createElement(NewTab, { 16868 store: store 16869 }), document.getElementById("root")); 16870 } 16871 function renderCache(initialState) { 16872 if (initialState) { 16873 initialState.App.isForStartupCache.App = false; 16874 } 16875 const store = initStore(reducers, initialState); 16876 new DetectUserSessionStart(store).sendEventOrAddListener(); 16877 doRequestWhenReady().then(() => { 16878 // If state events happened before we got here, 16879 // we can notify main that we need updates. 16880 // The individual feeds know what state is not cached. 16881 store.dispatch(actionCreators.OnlyToMain({ 16882 type: actionTypes.NEW_TAB_STATE_REQUEST_STARTUPCACHE 16883 })); 16884 }); 16885 external_ReactDOM_default().hydrate(/*#__PURE__*/external_React_default().createElement(NewTab, { 16886 store: store 16887 }), document.getElementById("root")); 16888 } 16889 NewtabRenderUtils = __webpack_exports__; 16890 /******/ })() 16891 ;