Reducers.sys.mjs (35694B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { actionTypes as at } from "resource://newtab/common/Actions.mjs"; 6 import { Dedupe } from "resource:///modules/Dedupe.sys.mjs"; 7 8 export { 9 TOP_SITES_DEFAULT_ROWS, 10 TOP_SITES_MAX_SITES_PER_ROW, 11 } from "resource:///modules/topsites/constants.mjs"; 12 13 const dedupe = new Dedupe(site => site && site.url); 14 15 export const INITIAL_STATE = { 16 App: { 17 // Have we received real data from the app yet? 18 initialized: false, 19 locale: "", 20 isForStartupCache: { 21 App: false, 22 TopSites: false, 23 DiscoveryStream: false, 24 Weather: false, 25 Wallpaper: false, 26 }, 27 customizeMenuVisible: false, 28 }, 29 Ads: { 30 initialized: false, 31 lastUpdated: null, 32 tiles: {}, 33 spocs: {}, 34 spocPlacements: {}, 35 }, 36 TopSites: { 37 // Have we received real data from history yet? 38 initialized: false, 39 // The history (and possibly default) links 40 rows: [], 41 // Used in content only to dispatch action to TopSiteForm. 42 editForm: null, 43 // Used in content only to open the SearchShortcutsForm modal. 44 showSearchShortcutsForm: false, 45 // The list of available search shortcuts. 46 searchShortcuts: [], 47 // The "Share-of-Voice" allocations generated by TopSitesFeed 48 sov: { 49 ready: false, 50 positions: [ 51 // {position: 0, assignedPartner: "amp"}, 52 // {position: 1, assignedPartner: "moz-sales"}, 53 ], 54 }, 55 }, 56 Prefs: { 57 initialized: false, 58 values: { featureConfig: {} }, 59 }, 60 Dialog: { 61 visible: false, 62 data: {}, 63 }, 64 Sections: [], 65 Pocket: { 66 pocketCta: {}, 67 waitingForSpoc: true, 68 }, 69 // This is the new pocket configurable layout state. 70 DiscoveryStream: { 71 // This is a JSON-parsed copy of the discoverystream.config pref value. 72 config: { enabled: false }, 73 layout: [], 74 topicsLoading: false, 75 feeds: { 76 data: { 77 // "https://foo.com/feed1": {lastUpdated: 123, data: [], personalized: false} 78 }, 79 loaded: false, 80 }, 81 // Used to show impressions in newtab devtools. 82 impressions: { 83 feed: {}, 84 }, 85 // Used to show blocks in newtab devtools. 86 blocks: {}, 87 spocs: { 88 spocs_endpoint: "", 89 lastUpdated: null, 90 cacheUpdateTime: null, 91 onDemand: { 92 enabled: false, 93 loaded: false, 94 }, 95 data: { 96 // "spocs": {title: "", context: "", items: [], personalized: false}, 97 // "placement1": {title: "", context: "", items: [], personalized: false}, 98 }, 99 loaded: false, 100 frequency_caps: [], 101 blocked: [], 102 placements: [], 103 }, 104 experimentData: { 105 utmSource: "pocket-newtab", 106 utmCampaign: undefined, 107 utmContent: undefined, 108 }, 109 showTopicSelection: false, 110 report: { 111 visible: false, 112 data: {}, 113 }, 114 sectionPersonalization: {}, 115 }, 116 // Messages received from ASRouter to render in newtab 117 Messages: { 118 // messages received from ASRouter are initially visible 119 isVisible: true, 120 // portID for that tab that was sent the message 121 portID: "", 122 // READONLY Message data received from ASRouter 123 messageData: {}, 124 }, 125 Notifications: { 126 showNotifications: false, 127 toastCounter: 0, 128 toastId: "", 129 // This queue is reset each time SHOW_TOAST_MESSAGE is ran. 130 // For can be a queue in the future, but for now is one item 131 toastQueue: [], 132 }, 133 Personalization: { 134 lastUpdated: null, 135 initialized: false, 136 }, 137 InferredPersonalization: { 138 initialized: false, 139 lastUpdated: null, 140 inferredInterests: {}, 141 coarseInferredInterests: {}, 142 coarsePrivateInferredInterests: {}, 143 }, 144 Search: { 145 // When search hand-off is enabled, we render a big button that is styled to 146 // look like a search textbox. If the button is clicked, we style 147 // the button as if it was a focused search box and show a fake cursor but 148 // really focus the awesomebar without the focus styles ("hidden focus"). 149 fakeFocus: false, 150 // Hide the search box after handing off to AwesomeBar and user starts typing. 151 hide: false, 152 }, 153 Wallpapers: { 154 wallpaperList: [], 155 highlightSeenCounter: 0, 156 categories: [], 157 uploadedWallpaper: "", 158 }, 159 Weather: { 160 initialized: false, 161 lastUpdated: null, 162 query: "", 163 suggestions: [], 164 locationData: { 165 city: "", 166 adminArea: "", 167 country: "", 168 }, 169 // Display search input in Weather widget 170 searchActive: false, 171 locationSearchString: "", 172 suggestedLocations: [], 173 }, 174 // Widgets 175 ListsWidget: { 176 // value pointing to last selectled list 177 selected: "taskList", 178 // Default state of an empty task list 179 lists: { 180 taskList: { 181 label: "", 182 tasks: [], 183 completed: [], 184 }, 185 }, 186 }, 187 TimerWidget: { 188 // The timer will have 2 types of states, focus and break. 189 // Focus will the default state 190 timerType: "focus", 191 focus: { 192 // Timer duration set by user; 25 mins by default 193 duration: 25 * 60, 194 // Initial duration - also set by the user; does not update until timer ends or user resets timer 195 initialDuration: 25 * 60, 196 // the Date.now() value when a user starts/resumes a timer 197 startTime: null, 198 // Boolean indicating if timer is currently running 199 isRunning: false, 200 }, 201 break: { 202 duration: 5 * 60, 203 initialDuration: 5 * 60, 204 startTime: null, 205 isRunning: false, 206 }, 207 }, 208 ExternalComponents: { 209 components: [], 210 }, 211 }; 212 213 function App(prevState = INITIAL_STATE.App, action) { 214 switch (action.type) { 215 case at.INIT: 216 return Object.assign({}, prevState, action.data || {}, { 217 initialized: true, 218 }); 219 case at.TOP_SITES_UPDATED: 220 // Toggle `isForStartupCache.TopSites` when receiving the `TOP_SITES_UPDATE` action 221 // so that sponsored tiles can be rendered as usual. See Bug 1826360. 222 return { 223 ...prevState, 224 isForStartupCache: { ...prevState.isForStartupCache, TopSites: false }, 225 }; 226 case at.DISCOVERY_STREAM_SPOCS_UPDATE: 227 // Toggle `isForStartupCache.DiscoveryStream` when receiving the `DISCOVERY_STREAM_SPOCS_UPDATE` action 228 // so that spoc cards can be rendered as usual. 229 return { 230 ...prevState, 231 isForStartupCache: { 232 ...prevState.isForStartupCache, 233 DiscoveryStream: false, 234 }, 235 }; 236 case at.WEATHER_UPDATE: 237 // Toggle `isForStartupCache.Weather` when receiving the `WEATHER_UPDATE` action 238 // so that weather can be rendered as usual. 239 return { 240 ...prevState, 241 isForStartupCache: { ...prevState.isForStartupCache, Weather: false }, 242 }; 243 case at.WALLPAPERS_CUSTOM_SET: 244 // Toggle `isForStartupCache.Wallpaper` when receiving the `WALLPAPERS_CUSTOM_SET` action 245 // so that custom wallpaper can be rendered as usual. 246 return { 247 ...prevState, 248 isForStartupCache: { ...prevState.isForStartupCache, Wallpaper: false }, 249 }; 250 case at.SHOW_PERSONALIZE: 251 return Object.assign({}, prevState, { 252 customizeMenuVisible: true, 253 }); 254 case at.HIDE_PERSONALIZE: 255 return Object.assign({}, prevState, { 256 customizeMenuVisible: false, 257 }); 258 default: 259 return prevState; 260 } 261 } 262 263 function TopSites(prevState = INITIAL_STATE.TopSites, action) { 264 let hasMatch; 265 let newRows; 266 switch (action.type) { 267 case at.TOP_SITES_UPDATED: 268 if (!action.data || !action.data.links) { 269 return prevState; 270 } 271 return Object.assign( 272 {}, 273 prevState, 274 { initialized: true, rows: action.data.links }, 275 action.data.pref ? { pref: action.data.pref } : {} 276 ); 277 case at.TOP_SITES_PREFS_UPDATED: 278 return Object.assign({}, prevState, { pref: action.data.pref }); 279 case at.TOP_SITES_EDIT: 280 return Object.assign({}, prevState, { 281 editForm: { 282 index: action.data.index, 283 previewResponse: null, 284 }, 285 }); 286 case at.TOP_SITES_CANCEL_EDIT: 287 return Object.assign({}, prevState, { editForm: null }); 288 case at.TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL: 289 return Object.assign({}, prevState, { showSearchShortcutsForm: true }); 290 case at.TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL: 291 return Object.assign({}, prevState, { showSearchShortcutsForm: false }); 292 case at.PREVIEW_RESPONSE: 293 if ( 294 !prevState.editForm || 295 action.data.url !== prevState.editForm.previewUrl 296 ) { 297 return prevState; 298 } 299 return Object.assign({}, prevState, { 300 editForm: { 301 index: prevState.editForm.index, 302 previewResponse: action.data.preview, 303 previewUrl: action.data.url, 304 }, 305 }); 306 case at.PREVIEW_REQUEST: 307 if (!prevState.editForm) { 308 return prevState; 309 } 310 return Object.assign({}, prevState, { 311 editForm: { 312 index: prevState.editForm.index, 313 previewResponse: null, 314 previewUrl: action.data.url, 315 }, 316 }); 317 case at.PREVIEW_REQUEST_CANCEL: 318 if (!prevState.editForm) { 319 return prevState; 320 } 321 return Object.assign({}, prevState, { 322 editForm: { 323 index: prevState.editForm.index, 324 previewResponse: null, 325 }, 326 }); 327 case at.SCREENSHOT_UPDATED: 328 newRows = prevState.rows.map(row => { 329 if (row && row.url === action.data.url) { 330 hasMatch = true; 331 return Object.assign({}, row, { screenshot: action.data.screenshot }); 332 } 333 return row; 334 }); 335 return hasMatch 336 ? Object.assign({}, prevState, { rows: newRows }) 337 : prevState; 338 case at.PLACES_BOOKMARK_ADDED: 339 if (!action.data) { 340 return prevState; 341 } 342 newRows = prevState.rows.map(site => { 343 if (site && site.url === action.data.url) { 344 const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data; 345 return Object.assign({}, site, { 346 bookmarkGuid, 347 bookmarkTitle, 348 bookmarkDateCreated: dateAdded, 349 }); 350 } 351 return site; 352 }); 353 return Object.assign({}, prevState, { rows: newRows }); 354 case at.PLACES_BOOKMARKS_REMOVED: 355 if (!action.data) { 356 return prevState; 357 } 358 newRows = prevState.rows.map(site => { 359 if (site && action.data.urls.includes(site.url)) { 360 const newSite = Object.assign({}, site); 361 delete newSite.bookmarkGuid; 362 delete newSite.bookmarkTitle; 363 delete newSite.bookmarkDateCreated; 364 return newSite; 365 } 366 return site; 367 }); 368 return Object.assign({}, prevState, { rows: newRows }); 369 case at.PLACES_LINKS_DELETED: 370 if (!action.data) { 371 return prevState; 372 } 373 newRows = prevState.rows.filter( 374 site => !action.data.urls.includes(site.url) 375 ); 376 return Object.assign({}, prevState, { rows: newRows }); 377 case at.UPDATE_SEARCH_SHORTCUTS: 378 return { ...prevState, searchShortcuts: action.data.searchShortcuts }; 379 case at.SOV_UPDATED: { 380 const sov = { 381 ready: action.data.ready, 382 positions: action.data.positions, 383 }; 384 return { ...prevState, sov }; 385 } 386 default: 387 return prevState; 388 } 389 } 390 391 function Dialog(prevState = INITIAL_STATE.Dialog, action) { 392 switch (action.type) { 393 case at.DIALOG_OPEN: 394 return Object.assign({}, prevState, { visible: true, data: action.data }); 395 case at.DIALOG_CANCEL: 396 return Object.assign({}, prevState, { visible: false }); 397 case at.DIALOG_CLOSE: 398 // Reset and hide the confirmation dialog once the action is complete. 399 return Object.assign({}, INITIAL_STATE.Dialog); 400 default: 401 return prevState; 402 } 403 } 404 405 function Prefs(prevState = INITIAL_STATE.Prefs, action) { 406 let newValues; 407 switch (action.type) { 408 case at.PREFS_INITIAL_VALUES: 409 return Object.assign({}, prevState, { 410 initialized: true, 411 values: action.data, 412 }); 413 case at.PREF_CHANGED: 414 newValues = Object.assign({}, prevState.values); 415 newValues[action.data.name] = action.data.value; 416 return Object.assign({}, prevState, { values: newValues }); 417 default: 418 return prevState; 419 } 420 } 421 422 function Sections(prevState = INITIAL_STATE.Sections, action) { 423 let hasMatch; 424 let newState; 425 switch (action.type) { 426 case at.SECTION_DEREGISTER: 427 return prevState.filter(section => section.id !== action.data); 428 case at.SECTION_REGISTER: 429 // If section exists in prevState, update it 430 newState = prevState.map(section => { 431 if (section && section.id === action.data.id) { 432 hasMatch = true; 433 return Object.assign({}, section, action.data); 434 } 435 return section; 436 }); 437 // Otherwise, append it 438 if (!hasMatch) { 439 const initialized = !!(action.data.rows && !!action.data.rows.length); 440 const section = Object.assign( 441 { title: "", rows: [], enabled: false }, 442 action.data, 443 { initialized } 444 ); 445 newState.push(section); 446 } 447 return newState; 448 case at.SECTION_UPDATE: 449 newState = prevState.map(section => { 450 if (section && section.id === action.data.id) { 451 // If the action is updating rows, we should consider initialized to be true. 452 // This can be overridden if initialized is defined in the action.data 453 const initialized = action.data.rows ? { initialized: true } : {}; 454 455 // Make sure pinned cards stay at their current position when rows are updated. 456 // Disabling a section (SECTION_UPDATE with empty rows) does not retain pinned cards. 457 if ( 458 action.data.rows && 459 !!action.data.rows.length && 460 section.rows.find(card => card.pinned) 461 ) { 462 const rows = Array.from(action.data.rows); 463 section.rows.forEach((card, index) => { 464 if (card.pinned) { 465 // Only add it if it's not already there. 466 if (rows[index].guid !== card.guid) { 467 rows.splice(index, 0, card); 468 } 469 } 470 }); 471 return Object.assign( 472 {}, 473 section, 474 initialized, 475 Object.assign({}, action.data, { rows }) 476 ); 477 } 478 479 return Object.assign({}, section, initialized, action.data); 480 } 481 return section; 482 }); 483 484 if (!action.data.dedupeConfigurations) { 485 return newState; 486 } 487 488 action.data.dedupeConfigurations.forEach(dedupeConf => { 489 newState = newState.map(section => { 490 if (section.id === dedupeConf.id) { 491 const dedupedRows = dedupeConf.dedupeFrom.reduce( 492 (rows, dedupeSectionId) => { 493 const dedupeSection = newState.find( 494 s => s.id === dedupeSectionId 495 ); 496 const [, newRows] = dedupe.group(dedupeSection.rows, rows); 497 return newRows; 498 }, 499 section.rows 500 ); 501 502 return Object.assign({}, section, { rows: dedupedRows }); 503 } 504 505 return section; 506 }); 507 }); 508 509 return newState; 510 case at.SECTION_UPDATE_CARD: 511 return prevState.map(section => { 512 if (section && section.id === action.data.id && section.rows) { 513 const newRows = section.rows.map(card => { 514 if (card.url === action.data.url) { 515 return Object.assign({}, card, action.data.options); 516 } 517 return card; 518 }); 519 return Object.assign({}, section, { rows: newRows }); 520 } 521 return section; 522 }); 523 case at.PLACES_BOOKMARK_ADDED: 524 if (!action.data) { 525 return prevState; 526 } 527 return prevState.map(section => 528 Object.assign({}, section, { 529 rows: section.rows.map(item => { 530 // find the item within the rows that is attempted to be bookmarked 531 if (item.url === action.data.url) { 532 const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data; 533 return Object.assign({}, item, { 534 bookmarkGuid, 535 bookmarkTitle, 536 bookmarkDateCreated: dateAdded, 537 type: "bookmark", 538 }); 539 } 540 return item; 541 }), 542 }) 543 ); 544 case at.PLACES_BOOKMARKS_REMOVED: 545 if (!action.data) { 546 return prevState; 547 } 548 return prevState.map(section => 549 Object.assign({}, section, { 550 rows: section.rows.map(item => { 551 // find the bookmark within the rows that is attempted to be removed 552 if (action.data.urls.includes(item.url)) { 553 const newSite = Object.assign({}, item); 554 delete newSite.bookmarkGuid; 555 delete newSite.bookmarkTitle; 556 delete newSite.bookmarkDateCreated; 557 if (!newSite.type || newSite.type === "bookmark") { 558 newSite.type = "history"; 559 } 560 return newSite; 561 } 562 return item; 563 }), 564 }) 565 ); 566 case at.PLACES_LINKS_DELETED: 567 if (!action.data) { 568 return prevState; 569 } 570 return prevState.map(section => 571 Object.assign({}, section, { 572 rows: section.rows.filter( 573 site => !action.data.urls.includes(site.url) 574 ), 575 }) 576 ); 577 case at.PLACES_LINK_BLOCKED: 578 if (!action.data) { 579 return prevState; 580 } 581 return prevState.map(section => 582 Object.assign({}, section, { 583 rows: section.rows.filter(site => site.url !== action.data.url), 584 }) 585 ); 586 default: 587 return prevState; 588 } 589 } 590 591 function Messages(prevState = INITIAL_STATE.Messages, action) { 592 switch (action.type) { 593 case at.MESSAGE_SET: 594 if (prevState.messageData.messageType) { 595 return prevState; 596 } 597 return { 598 ...prevState, 599 messageData: action.data.message, 600 portID: action.data.portID || "", 601 }; 602 case at.MESSAGE_TOGGLE_VISIBILITY: 603 return { ...prevState, isVisible: action.data }; 604 default: 605 return prevState; 606 } 607 } 608 609 function Pocket(prevState = INITIAL_STATE.Pocket, action) { 610 switch (action.type) { 611 case at.POCKET_WAITING_FOR_SPOC: 612 return { ...prevState, waitingForSpoc: action.data }; 613 case at.POCKET_CTA: 614 return { 615 ...prevState, 616 pocketCta: { 617 ctaButton: action.data.cta_button, 618 ctaText: action.data.cta_text, 619 ctaUrl: action.data.cta_url, 620 useCta: action.data.use_cta, 621 }, 622 }; 623 default: 624 return prevState; 625 } 626 } 627 628 function Personalization(prevState = INITIAL_STATE.Personalization, action) { 629 switch (action.type) { 630 case at.DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED: 631 return { 632 ...prevState, 633 lastUpdated: action.data.lastUpdated, 634 }; 635 case at.DISCOVERY_STREAM_PERSONALIZATION_INIT: 636 return { 637 ...prevState, 638 initialized: true, 639 }; 640 case at.DISCOVERY_STREAM_PERSONALIZATION_RESET: 641 return { ...INITIAL_STATE.Personalization }; 642 default: 643 return prevState; 644 } 645 } 646 647 function InferredPersonalization( 648 prevState = INITIAL_STATE.InferredPersonalization, 649 action 650 ) { 651 switch (action.type) { 652 case at.INFERRED_PERSONALIZATION_UPDATE: 653 return { 654 ...prevState, 655 initialized: true, 656 inferredInterests: action.data.inferredInterests, 657 coarseInferredInterests: action.data.coarseInferredInterests, 658 coarsePrivateInferredInterests: 659 action.data.coarsePrivateInferredInterests, 660 lastUpdated: action.data.lastUpdated, 661 }; 662 case at.INFERRED_PERSONALIZATION_RESET: 663 return { ...INITIAL_STATE.InferredPersonalization }; 664 default: 665 return prevState; 666 } 667 } 668 669 // eslint-disable-next-line complexity 670 function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) { 671 // Return if action data is empty, or spocs or feeds data is not loaded 672 const isNotReady = () => 673 !action.data || !prevState.spocs.loaded || !prevState.feeds.loaded; 674 675 const handlePlacements = handleSites => { 676 const { data, placements } = prevState.spocs; 677 const result = {}; 678 679 const forPlacement = placement => { 680 const placementSpocs = data[placement.name]; 681 682 if ( 683 !placementSpocs || 684 !placementSpocs.items || 685 !placementSpocs.items.length 686 ) { 687 return; 688 } 689 690 result[placement.name] = { 691 ...placementSpocs, 692 items: handleSites(placementSpocs.items), 693 }; 694 }; 695 696 if (!placements || !placements.length) { 697 [{ name: "spocs" }].forEach(forPlacement); 698 } else { 699 placements.forEach(forPlacement); 700 } 701 return result; 702 }; 703 704 const nextState = handleSites => ({ 705 ...prevState, 706 spocs: { 707 ...prevState.spocs, 708 data: handlePlacements(handleSites), 709 }, 710 feeds: { 711 ...prevState.feeds, 712 data: Object.keys(prevState.feeds.data).reduce( 713 (accumulator, feed_url) => { 714 accumulator[feed_url] = { 715 data: { 716 ...prevState.feeds.data[feed_url].data, 717 recommendations: handleSites( 718 prevState.feeds.data[feed_url].data.recommendations 719 ), 720 }, 721 }; 722 return accumulator; 723 }, 724 {} 725 ), 726 }, 727 }); 728 729 switch (action.type) { 730 case at.DISCOVERY_STREAM_CONFIG_CHANGE: 731 // Fall through to a separate action is so it doesn't trigger a listener update on init 732 case at.DISCOVERY_STREAM_CONFIG_SETUP: 733 return { ...prevState, config: action.data || {} }; 734 case at.DISCOVERY_STREAM_EXPERIMENT_DATA: 735 return { ...prevState, experimentData: action.data || {} }; 736 case at.DISCOVERY_STREAM_LAYOUT_UPDATE: 737 return { 738 ...prevState, 739 layout: action.data.layout || [], 740 }; 741 case at.DISCOVERY_STREAM_TOPICS_LOADING: 742 return { 743 ...prevState, 744 topicsLoading: action.data, 745 }; 746 case at.DISCOVERY_STREAM_PREFS_SETUP: 747 return { 748 ...prevState, 749 hideDescriptions: action.data.hideDescriptions, 750 compactImages: action.data.compactImages, 751 imageGradient: action.data.imageGradient, 752 newSponsoredLabel: action.data.newSponsoredLabel, 753 titleLines: action.data.titleLines, 754 descLines: action.data.descLines, 755 readTime: action.data.readTime, 756 }; 757 case at.SHOW_PRIVACY_INFO: 758 return { 759 ...prevState, 760 }; 761 case at.DISCOVERY_STREAM_LAYOUT_RESET: 762 return { ...INITIAL_STATE.DiscoveryStream, config: prevState.config }; 763 case at.DISCOVERY_STREAM_FEEDS_UPDATE: 764 return { 765 ...prevState, 766 feeds: { 767 ...prevState.feeds, 768 loaded: true, 769 }, 770 }; 771 case at.DISCOVERY_STREAM_FEED_UPDATE: { 772 const newData = {}; 773 newData[action.data.url] = action.data.feed; 774 return { 775 ...prevState, 776 feeds: { 777 ...prevState.feeds, 778 data: { 779 ...prevState.feeds.data, 780 ...newData, 781 }, 782 }, 783 }; 784 } 785 case at.DISCOVERY_STREAM_DEV_IMPRESSIONS: 786 return { 787 ...prevState, 788 impressions: { 789 ...prevState.impressions, 790 feed: action.data, 791 }, 792 }; 793 case at.DISCOVERY_STREAM_DEV_BLOCKS: 794 return { 795 ...prevState, 796 blocks: action.data, 797 }; 798 case at.DISCOVERY_STREAM_SPOCS_CAPS: 799 return { 800 ...prevState, 801 spocs: { 802 ...prevState.spocs, 803 frequency_caps: [...prevState.spocs.frequency_caps, ...action.data], 804 }, 805 }; 806 case at.DISCOVERY_STREAM_SPOCS_ENDPOINT: 807 return { 808 ...prevState, 809 spocs: { 810 ...INITIAL_STATE.DiscoveryStream.spocs, 811 spocs_endpoint: 812 action.data.url || 813 INITIAL_STATE.DiscoveryStream.spocs.spocs_endpoint, 814 }, 815 }; 816 case at.DISCOVERY_STREAM_SPOCS_PLACEMENTS: 817 return { 818 ...prevState, 819 spocs: { 820 ...prevState.spocs, 821 placements: 822 action.data.placements || 823 INITIAL_STATE.DiscoveryStream.spocs.placements, 824 }, 825 }; 826 case at.DISCOVERY_STREAM_SPOCS_UPDATE: 827 if (action.data) { 828 // If spocs have been loaded on this tab, we can ignore future updates. 829 // This should never be true on the main store, only content pages. 830 // We check agasint onDemand just to be safe. It generally shouldn't be needed. 831 if (prevState.spocs?.onDemand?.loaded) { 832 return prevState; 833 } 834 return { 835 ...prevState, 836 spocs: { 837 ...prevState.spocs, 838 lastUpdated: action.data.lastUpdated, 839 data: action.data.spocs, 840 cacheUpdateTime: action.data.spocsCacheUpdateTime, 841 onDemand: { 842 enabled: action.data.spocsOnDemand, 843 loaded: false, 844 }, 845 loaded: true, 846 }, 847 }; 848 } 849 return prevState; 850 case at.DISCOVERY_STREAM_SPOCS_ONDEMAND_LOAD: 851 return { 852 ...prevState, 853 spocs: { 854 ...prevState.spocs, 855 onDemand: { 856 ...prevState.spocs.onDemand, 857 loaded: true, 858 }, 859 }, 860 }; 861 case at.DISCOVERY_STREAM_SPOCS_ONDEMAND_RESET: 862 if (action.data) { 863 return { 864 ...prevState, 865 spocs: { 866 ...prevState.spocs, 867 cacheUpdateTime: action.data.spocsCacheUpdateTime, 868 onDemand: { 869 ...prevState.spocs.onDemand, 870 enabled: action.data.spocsOnDemand, 871 }, 872 }, 873 }; 874 } 875 return prevState; 876 case at.DISCOVERY_STREAM_SPOC_BLOCKED: 877 return { 878 ...prevState, 879 spocs: { 880 ...prevState.spocs, 881 blocked: [...prevState.spocs.blocked, action.data.url], 882 }, 883 }; 884 case at.DISCOVERY_STREAM_LINK_BLOCKED: 885 return isNotReady() 886 ? prevState 887 : nextState(items => 888 items.filter(item => item.url !== action.data.url) 889 ); 890 891 case at.PLACES_BOOKMARK_ADDED: { 892 const updateBookmarkInfo = item => { 893 if (item.url === action.data.url) { 894 const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data; 895 return Object.assign({}, item, { 896 bookmarkGuid, 897 bookmarkTitle, 898 bookmarkDateCreated: dateAdded, 899 context_type: "bookmark", 900 }); 901 } 902 return item; 903 }; 904 return isNotReady() 905 ? prevState 906 : nextState(items => items.map(updateBookmarkInfo)); 907 } 908 case at.PLACES_BOOKMARKS_REMOVED: { 909 const removeBookmarkInfo = item => { 910 if (action.data.urls.includes(item.url)) { 911 const newSite = Object.assign({}, item); 912 delete newSite.bookmarkGuid; 913 delete newSite.bookmarkTitle; 914 delete newSite.bookmarkDateCreated; 915 if (!newSite.context_type || newSite.context_type === "bookmark") { 916 newSite.context_type = "removedBookmark"; 917 } 918 return newSite; 919 } 920 return item; 921 }; 922 return isNotReady() 923 ? prevState 924 : nextState(items => items.map(removeBookmarkInfo)); 925 } 926 case at.TOPIC_SELECTION_SPOTLIGHT_OPEN: 927 return { 928 ...prevState, 929 showTopicSelection: true, 930 }; 931 case at.TOPIC_SELECTION_SPOTLIGHT_CLOSE: 932 return { 933 ...prevState, 934 showTopicSelection: false, 935 }; 936 case at.SECTION_BLOCKED: 937 return { 938 ...prevState, 939 showBlockSectionConfirmation: true, 940 sectionPersonalization: action.data, 941 }; 942 case at.REPORT_AD_OPEN: 943 return { 944 ...prevState, 945 report: { 946 ...prevState.report, 947 card_type: action.data?.card_type, 948 position: action.data?.position, 949 placement_id: action.data?.placement_id, 950 reporting_url: action.data?.reporting_url, 951 url: action.data?.url, 952 visible: true, 953 }, 954 }; 955 case at.REPORT_CONTENT_OPEN: 956 return { 957 ...prevState, 958 report: { 959 ...prevState.report, 960 card_type: action.data?.card_type, 961 corpus_item_id: action.data?.corpus_item_id, 962 scheduled_corpus_item_id: action.data?.scheduled_corpus_item_id, 963 section_position: action.data?.section_position, 964 section: action.data?.section, 965 title: action.data?.title, 966 topic: action.data?.topic, 967 url: action.data?.url, 968 visible: true, 969 }, 970 }; 971 case at.REPORT_CLOSE: 972 case at.REPORT_AD_SUBMIT: 973 case at.REPORT_CONTENT_SUBMIT: 974 return { 975 ...prevState, 976 report: { 977 ...prevState.report, 978 visible: false, 979 }, 980 }; 981 case at.SECTION_PERSONALIZATION_UPDATE: 982 return { ...prevState, sectionPersonalization: action.data }; 983 default: 984 return prevState; 985 } 986 } 987 988 function Search(prevState = INITIAL_STATE.Search, action) { 989 switch (action.type) { 990 case at.DISABLE_SEARCH: 991 return Object.assign({ ...prevState, disable: true }); 992 case at.FAKE_FOCUS_SEARCH: 993 return Object.assign({ ...prevState, fakeFocus: true }); 994 case at.SHOW_SEARCH: 995 return Object.assign({ ...prevState, disable: false, fakeFocus: false }); 996 default: 997 return prevState; 998 } 999 } 1000 1001 function Wallpapers(prevState = INITIAL_STATE.Wallpapers, action) { 1002 switch (action.type) { 1003 case at.WALLPAPERS_SET: 1004 return { 1005 ...prevState, 1006 wallpaperList: action.data, 1007 }; 1008 case at.WALLPAPERS_FEATURE_HIGHLIGHT_COUNTER_INCREMENT: 1009 return { 1010 ...prevState, 1011 highlightSeenCounter: action.data, 1012 }; 1013 case at.WALLPAPERS_CATEGORY_SET: 1014 return { ...prevState, categories: action.data }; 1015 case at.WALLPAPERS_CUSTOM_SET: 1016 return { ...prevState, uploadedWallpaper: action.data }; 1017 default: 1018 return prevState; 1019 } 1020 } 1021 1022 function Notifications(prevState = INITIAL_STATE.Notifications, action) { 1023 switch (action.type) { 1024 case at.SHOW_TOAST_MESSAGE: 1025 return { 1026 ...prevState, 1027 showNotifications: action.data.showNotifications, 1028 toastCounter: prevState.toastCounter + 1, 1029 toastId: action.data.toastId, 1030 toastQueue: [action.data.toastId], 1031 }; 1032 case at.HIDE_TOAST_MESSAGE: { 1033 const { showNotifications, toastId: hiddenToastId } = action.data; 1034 const queuedToasts = [...prevState.toastQueue].filter( 1035 toastId => toastId !== hiddenToastId 1036 ); 1037 return { 1038 ...prevState, 1039 toastCounter: queuedToasts.length, 1040 toastQueue: queuedToasts, 1041 toastId: "", 1042 showNotifications, 1043 }; 1044 } 1045 default: 1046 return prevState; 1047 } 1048 } 1049 1050 function Weather(prevState = INITIAL_STATE.Weather, action) { 1051 switch (action.type) { 1052 case at.WEATHER_UPDATE: 1053 return { 1054 ...prevState, 1055 suggestions: action.data.suggestions, 1056 lastUpdated: action.data.date, 1057 locationData: action.data.locationData || prevState.locationData, 1058 initialized: true, 1059 }; 1060 case at.WEATHER_SEARCH_ACTIVE: 1061 return { ...prevState, searchActive: action.data }; 1062 case at.WEATHER_LOCATION_SEARCH_UPDATE: 1063 return { ...prevState, locationSearchString: action.data }; 1064 case at.WEATHER_LOCATION_SUGGESTIONS_UPDATE: 1065 return { ...prevState, suggestedLocations: action.data }; 1066 case at.WEATHER_LOCATION_DATA_UPDATE: 1067 return { ...prevState, locationData: action.data }; 1068 default: 1069 return prevState; 1070 } 1071 } 1072 1073 function Ads(prevState = INITIAL_STATE.Ads, action) { 1074 switch (action.type) { 1075 case at.ADS_INIT: 1076 return { 1077 ...prevState, 1078 initialized: true, 1079 }; 1080 case at.ADS_UPDATE_TILES: 1081 return { 1082 ...prevState, 1083 tiles: action.data.tiles, 1084 }; 1085 case at.ADS_UPDATE_SPOCS: 1086 return { 1087 ...prevState, 1088 spocs: action.data.spocs, 1089 spocPlacements: action.data.spocPlacements, 1090 }; 1091 case at.ADS_RESET: 1092 return { ...INITIAL_STATE.Ads }; 1093 default: 1094 return prevState; 1095 } 1096 } 1097 1098 function TimerWidget(prevState = INITIAL_STATE.TimerWidget, action) { 1099 // fallback to current timerType in state if not provided in action 1100 const timerType = action.data?.timerType || prevState.timerType; 1101 switch (action.type) { 1102 case at.WIDGETS_TIMER_SET: 1103 return { 1104 ...prevState, 1105 ...action.data, 1106 }; 1107 case at.WIDGETS_TIMER_SET_TYPE: 1108 return { 1109 ...prevState, 1110 timerType: action.data.timerType, 1111 }; 1112 case at.WIDGETS_TIMER_SET_DURATION: 1113 return { 1114 ...prevState, 1115 [timerType]: { 1116 // setting a dynamic key assignment to let us dynamically update timer type's state based on what is set 1117 duration: action.data.duration, 1118 initialDuration: action.data.duration, 1119 startTime: null, 1120 isRunning: false, 1121 }, 1122 }; 1123 case at.WIDGETS_TIMER_PLAY: 1124 return { 1125 ...prevState, 1126 [timerType]: { 1127 ...prevState[timerType], 1128 startTime: Math.floor(Date.now() / 1000), // reflected in seconds 1129 isRunning: true, 1130 }, 1131 }; 1132 case at.WIDGETS_TIMER_PAUSE: 1133 if (prevState[timerType]?.isRunning) { 1134 return { 1135 ...prevState, 1136 [timerType]: { 1137 ...prevState[timerType], 1138 duration: action.data.duration, 1139 // setting startTime to null on pause because we need to check the exact time the user presses play, 1140 // whether it's when the user starts or resumes the timer. This helps get accurate results 1141 startTime: null, 1142 isRunning: false, 1143 }, 1144 }; 1145 } 1146 return prevState; 1147 case at.WIDGETS_TIMER_RESET: 1148 return { 1149 ...prevState, 1150 [timerType]: { 1151 ...prevState[timerType], 1152 duration: action.data.duration, 1153 initialDuration: action.data.duration, 1154 startTime: null, 1155 isRunning: false, 1156 }, 1157 }; 1158 case at.WIDGETS_TIMER_END: 1159 return { 1160 ...prevState, 1161 [timerType]: { 1162 ...prevState[timerType], 1163 duration: action.data.duration, 1164 initialDuration: action.data.duration, 1165 startTime: null, 1166 isRunning: false, 1167 }, 1168 }; 1169 default: 1170 return prevState; 1171 } 1172 } 1173 1174 function ListsWidget(prevState = INITIAL_STATE.ListsWidget, action) { 1175 switch (action.type) { 1176 case at.WIDGETS_LISTS_SET: 1177 return { ...prevState, lists: action.data }; 1178 case at.WIDGETS_LISTS_SET_SELECTED: 1179 return { ...prevState, selected: action.data }; 1180 default: 1181 return prevState; 1182 } 1183 } 1184 1185 function ExternalComponents( 1186 prevState = INITIAL_STATE.ExternalComponents, 1187 action 1188 ) { 1189 switch (action.type) { 1190 case at.REFRESH_EXTERNAL_COMPONENTS: 1191 return { ...prevState, components: action.data }; 1192 default: 1193 return prevState; 1194 } 1195 } 1196 1197 export const reducers = { 1198 TopSites, 1199 App, 1200 Ads, 1201 Prefs, 1202 Dialog, 1203 Sections, 1204 Messages, 1205 Notifications, 1206 Pocket, 1207 Personalization, 1208 InferredPersonalization, 1209 DiscoveryStream, 1210 Search, 1211 TimerWidget, 1212 ListsWidget, 1213 Wallpapers, 1214 Weather, 1215 ExternalComponents, 1216 };