tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 };